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

@ -1,21 +1,21 @@
.About.Dialog { .About.Dialog {
width: $unit * 60; width: $unit * 60;
section { section {
margin-bottom: $unit; margin-bottom: $unit;
h2 { h2 {
margin-bottom: $unit * 3; margin-bottom: $unit * 3;
}
} }
}
.DialogDescription { .DialogDescription {
font-size: $font-regular; font-size: $font-regular;
line-height: 1.24; line-height: 1.24;
margin-bottom: $unit; margin-bottom: $unit;
&:last-of-type { &:last-of-type {
margin-bottom: 0; margin-bottom: 0;
}
} }
}
} }

View file

@ -6,56 +6,68 @@ import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
const AboutModal = () => { const AboutModal = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
return ( return (
<Dialog.Root> <Dialog.Root>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('modals.about.title')}</span> <span>{t('modals.about.title')}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
<div className="DialogHeader"> className="About Dialog"
<Dialog.Title className="DialogTitle">{t('menu.about')}</Dialog.Title> onOpenAutoFocus={(event) => event.preventDefault()}
<Dialog.Close className="DialogClose" asChild> >
<span> <div className="DialogHeader">
<CrossIcon /> <Dialog.Title className="DialogTitle">
</span> {t('menu.about')}
</Dialog.Close> </Dialog.Title>
</div> <Dialog.Close className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</Dialog.Close>
</div>
<section> <section>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
Granblue.team is a tool to save and share team compositions for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a> Granblue.team is a tool to save and share team compositions for{' '}
</Dialog.Description> <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
<Dialog.Description className="DialogDescription"> </Dialog.Description>
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 className="DialogDescription">
</Dialog.Description> Start adding things to a team and a URL will be created for you to
<Dialog.Description className="DialogDescription"> share it wherever you like, no account needed.
You can make an account to save any teams you find for future reference, or to keep all of your teams together in one place. </Dialog.Description>
</Dialog.Description> <Dialog.Description className="DialogDescription">
</section> 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> <section>
<Dialog.Title className="DialogTitle">Credits</Dialog.Title> <Dialog.Title className="DialogTitle">Credits</Dialog.Title>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
Granblue.team was built by <a href="https://twitter.com/jedmund">@jedmund</a> with a lot of help from <a href="https://twitter.com/lalalalinna">@lalalalinna</a> and <a href="https://twitter.com/tarngerine">@tarngerine</a>. Granblue.team was built by{' '}
</Dialog.Description> <a href="https://twitter.com/jedmund">@jedmund</a> with a lot of
</section> help from{' '}
<a href="https://twitter.com/lalalalinna">@lalalalinna</a> and{' '}
<a href="https://twitter.com/tarngerine">@tarngerine</a>.
</Dialog.Description>
</section>
<section> <section>
<Dialog.Title className="DialogTitle">Open Source</Dialog.Title> <Dialog.Title className="DialogTitle">Open Source</Dialog.Title>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
This app is open source. You can contribute on Github. This app is open source. You can contribute on Github.
</Dialog.Description> </Dialog.Description>
</section> </section>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
) )
} }
export default AboutModal export default AboutModal

View file

@ -1,164 +1,142 @@
.Account.Dialog { .Account.Dialog {
display: flex;
flex-direction: column;
gap: $unit * 2;
width: $unit * 60;
form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 2; gap: $unit * 2;
width: $unit * 60;
form { .Switch {
$height: 34px;
background: $grey-70;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 58px;
height: $height;
&:focus {
box-shadow: 0 0 0 2px $grey-15;
}
&[data-state='checked'] {
background: $grey-15;
}
}
.Thumb {
background: $grey-100;
border-radius: 13px;
display: block;
height: 26px;
width: 26px;
transition: transform 100ms;
transform: translateX(-1px);
&:hover {
cursor: pointer;
}
&[data-state='checked'] {
background: $grey-100;
transform: translateX(21px);
}
}
.field {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit * 2;
select {
background: no-repeat url('/icons/ArrowDark.svg'), $grey-90;
background-position-y: center;
background-position-x: 95%;
margin: 0;
width: 240px;
}
.left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 2; flex-grow: 1;
gap: calc($unit / 2);
.Switch { label {
$height: 34px; color: var(--text-secondary);
background: $grey-70; font-size: $font-regular;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 58px;
height: $height;
&:focus {
box-shadow: 0 0 0 2px $grey-00;
}
&[data-state="checked"] {
background: $grey-00;
}
} }
.Thumb { p {
background: white; color: var(--text-secondary);
border-radius: 13px; font-size: $font-small;
display: block; line-height: 1.1;
height: 26px; max-width: 300px;
width: 26px;
transition: transform 100ms;
transform: translateX(-1px);
&:hover { &.jp {
cursor: pointer; max-width: 270px;
} }
}
}
&[data-state="checked"] { .preview {
background: white; $diameter: 48px;
transform: translateX(21px); background-color: $grey-90;
} border-radius: 999px;
height: $diameter;
width: $diameter;
img {
height: $diameter;
width: $diameter;
} }
.Button { &.fire {
font-size: $font-regular; background: $fire-bg-20;
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 { &.water {
align-items: center; background: $water-bg-20;
display: flex;
flex-direction: row;
gap: $unit * 2;
select {
background: no-repeat url('/icons/ArrowDark.svg'), $grey-90;
background-position-y: center;
background-position-x: 95%;
margin: 0;
width: 240px;
}
.left {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
label {
color: $grey-00;
font-size: $font-regular;
}
p {
color: $grey-60;
font-size: $font-small;
line-height: 1.1;
max-width: 300px;
&.jp {
max-width: 270px;
}
}
}
.preview {
$diameter: 48px;
background-color: $grey-90;
border-radius: 999px;
height: $diameter;
width: $diameter;
img {
height: $diameter;
width: $diameter;
}
&.fire {
background: $fire-bg-light;
}
&.water {
background: $water-bg-light;
}
&.wind {
background: $wind-bg-light;
}
&.earth {
background: $earth-bg-light;
}
&.dark {
background: $dark-bg-light;
}
&.light {
background: $light-bg-light;
}
}
} }
section { &.wind {
margin-bottom: $unit; background: $wind-bg-20;
h2 {
margin-bottom: $unit * 3;
}
} }
&.earth {
background: $earth-bg-20;
}
&.dark {
background: $dark-bg-10;
}
&.light {
background: $light-bg-20;
}
}
} }
.DialogDescription { section {
font-size: $font-regular; margin-bottom: $unit;
line-height: 1.24;
margin-bottom: $unit;
&:last-of-type { h2 {
margin-bottom: 0; margin-bottom: $unit * 3;
} }
} }
}
.DialogDescription {
font-size: $font-regular;
line-height: 1.24;
margin-bottom: $unit;
&:last-of-type {
margin-bottom: 0;
}
}
} }

View file

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

View file

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

View file

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

View file

@ -1,48 +1,48 @@
.AXSelect { .AXSelect {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit; gap: $unit;
.AXSet { .AXSet {
&.hidden { &.hidden {
display: none; display: none;
}
.errors {
color: $error;
display: none;
padding: $unit 0;
&.visible {
display: block;
}
}
.fields {
display: flex;
flex-direction: row;
gap: $unit;
select {
flex-grow: 1;
margin: 0;
}
.Input {
-webkit-font-smoothing: antialiased;
border: none;
background-color: $grey-90;
border-radius: 6px;
box-sizing: border-box;
color: $grey-00;
height: $unit * 6;
display: block;
font-size: $font-regular;
padding: $unit;
text-align: right;
min-width: 100px;
width: 100px;
}
}
} }
.errors {
color: $error;
display: none;
padding: $unit 0;
&.visible {
display: block;
}
}
.fields {
display: flex;
flex-direction: row;
gap: $unit;
select {
flex-grow: 1;
margin: 0;
}
.Input {
-webkit-font-smoothing: antialiased;
border: none;
background-color: $grey-90;
border-radius: 6px;
box-sizing: border-box;
color: $grey-15;
height: $unit * 6;
display: block;
font-size: $font-regular;
padding: $unit;
text-align: right;
min-width: 100px;
width: 100px;
}
}
}
} }

View file

@ -9,258 +9,338 @@ import { axData } from '~utils/axData'
import './index.scss' import './index.scss'
interface ErrorMap { interface ErrorMap {
[index: string]: string [index: string]: string
axValue1: string axValue1: string
axValue2: string axValue2: string
} }
interface Props { interface Props {
axType: number axType: number
currentSkills?: SimpleAxSkill[], currentSkills?: SimpleAxSkill[]
sendValidity: (isValid: boolean) => void sendValidity: (isValid: boolean) => void
sendValues: (primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) => void sendValues: (
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) => void
} }
const AXSelect = (props: Props) => { const AXSelect = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
const { t } = useTranslation('common') router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Set up form states and error handling // Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
axValue1: '', axValue1: '',
axValue2: '' axValue2: '',
}) })
const primaryErrorClasses = classNames({ const primaryErrorClasses = classNames({
'errors': true, errors: true,
'visible': errors.axValue1.length > 0 visible: errors.axValue1.length > 0,
}) })
const secondaryErrorClasses = classNames({ const secondaryErrorClasses = classNames({
'errors': true, errors: true,
'visible': errors.axValue2.length > 0 visible: errors.axValue2.length > 0,
}) })
// Refs // Refs
const primaryAxModifierSelect = React.createRef<HTMLSelectElement>() const primaryAxModifierSelect = React.createRef<HTMLSelectElement>()
const primaryAxValueInput = React.createRef<HTMLInputElement>() const primaryAxValueInput = React.createRef<HTMLInputElement>()
const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>() const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>()
const secondaryAxValueInput = React.createRef<HTMLInputElement>() const secondaryAxValueInput = React.createRef<HTMLInputElement>()
// States // States
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1) const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1) const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0) const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0) const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
useEffect(() => { useEffect(() => {
if (props.currentSkills && props.currentSkills[0]) { if (props.currentSkills && props.currentSkills[0]) {
if (props.currentSkills[0].modifier != null) if (props.currentSkills[0].modifier != null)
setPrimaryAxModifier(props.currentSkills[0].modifier) setPrimaryAxModifier(props.currentSkills[0].modifier)
setPrimaryAxValue(props.currentSkills[0].strength) setPrimaryAxValue(props.currentSkills[0].strength)
}
if (props.currentSkills && props.currentSkills[1]) {
if (props.currentSkills[1].modifier != null)
setSecondaryAxModifier(props.currentSkills[1].modifier)
setSecondaryAxValue(props.currentSkills[1].strength)
}
}, [props.currentSkills])
useEffect(() => {
props.sendValues(primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue)
}, [props, primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue])
useEffect(() => {
props.sendValidity(primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === '')
}, [props, primaryAxValue, errors])
// Classes
const secondarySetClasses = classNames({
'AXSet': true,
'hidden': primaryAxModifier < 0
})
function generateOptions(modifierSet: number) {
const axOptions = axData[props.axType - 1]
let axOptionElements: React.ReactNode[] = []
if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name[locale]}</option>
)
})
} else {
// If we are loading data from the server, state doesn't set before render,
// so our defaultValue is undefined.
let modifier = -1;
if (primaryAxModifier >= 0)
modifier = primaryAxModifier
else if (props.currentSkills)
modifier = props.currentSkills[0].modifier
if (modifier >= 0 && axOptions[modifier]) {
const primarySkill = axOptions[modifier]
if (primarySkill.secondary) {
const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name[locale]}</option>
)
})
}
}
}
axOptionElements?.unshift(<option key={-1} value={-1}>{t('ax.no_skill')}</option>)
return axOptionElements
} }
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) { if (props.currentSkills && props.currentSkills[1]) {
const value = parseInt(event.target.value) if (props.currentSkills[1].modifier != null)
setSecondaryAxModifier(props.currentSkills[1].modifier)
if (primaryAxModifierSelect.current == event.target) { setSecondaryAxValue(props.currentSkills[1].strength)
setPrimaryAxModifier(value)
if (primaryAxValueInput.current && secondaryAxModifierSelect.current && secondaryAxValueInput.current) {
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
secondaryAxModifierSelect.current.value = "-1"
secondaryAxValueInput.current.value = ""
}
} else {
setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
const currentAxSkill = (primaryAxSkill.secondary) ?
primaryAxSkill.secondary.find(skill => skill.id == value) : undefined
if (secondaryAxValueInput.current)
setupInput(currentAxSkill, secondaryAxValueInput.current)
}
} }
}, [props.currentSkills])
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) { useEffect(() => {
const value = parseFloat(event.target.value) props.sendValues(
let newErrors = {...errors} primaryAxModifier,
primaryAxValue,
if (primaryAxValueInput.current == event.target) { secondaryAxModifier,
if (handlePrimaryErrors(value)) secondaryAxValue
setPrimaryAxValue(value)
} else {
if (handleSecondaryErrors(value))
setSecondaryAxValue(value)
}
}
function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors}
if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = t('ax.errors.value_too_low', {
name: primaryAxSkill.name[locale],
minValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
})
} else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = t('ax.errors.value_too_high', {
name: primaryAxSkill.name[locale],
maxValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
})
} else if (!value || value <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else {
newErrors.axValue1 = ''
}
setErrors(newErrors)
return newErrors.axValue1.length === 0
}
function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors}
if (primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondaryAxModifier)
if (secondaryAxSkill) {
if (value < secondaryAxSkill.minValue) {
newErrors.axValue2 = t('ax.errors.value_too_low', {
name: secondaryAxSkill.name[locale],
minValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
})
} else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = t('ax.errors.value_too_high', {
name: secondaryAxSkill.name[locale],
maxValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
})
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = t('ax.errors.value_not_whole', { name: secondaryAxSkill.name[locale] })
} else if (primaryAxValue <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else {
newErrors.axValue2 = ''
}
}
}
setErrors(newErrors)
return newErrors.axValue2.length === 0
}
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
if (ax) {
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`
element.disabled = false
element.placeholder = rangeString
element.min = `${ax.minValue}`
element.max = `${ax.maxValue}`
element.step = (ax.suffix) ? "0.5" : "1"
} else {
if (primaryAxValueInput.current && secondaryAxValueInput.current) {
if (primaryAxValueInput.current == element) {
primaryAxValueInput.current.disabled = true
primaryAxValueInput.current.placeholder = ''
}
secondaryAxValueInput.current.disabled = true
secondaryAxValueInput.current.placeholder = ''
}
}
}
return (
<div className="AXSelect">
<div className="AXSet">
<div className="fields">
<select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled={primaryAxValue != 0} />
</div>
<p className={primaryErrorClasses}>{errors.axValue1}</p>
</div>
<div className={secondarySetClasses}>
<div className="fields">
<select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled={secondaryAxValue != 0} />
</div>
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div>
</div>
) )
}, [
props,
primaryAxModifier,
primaryAxValue,
secondaryAxModifier,
secondaryAxValue,
])
useEffect(() => {
props.sendValidity(
primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === ''
)
}, [props, primaryAxValue, errors])
// Classes
const secondarySetClasses = classNames({
AXSet: true,
hidden: primaryAxModifier < 0,
})
function generateOptions(modifierSet: number) {
const axOptions = axData[props.axType - 1]
let axOptionElements: React.ReactNode[] = []
if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>
{ax.name[locale]}
</option>
)
})
} else {
// If we are loading data from the server, state doesn't set before render,
// so our defaultValue is undefined.
let modifier = -1
if (primaryAxModifier >= 0) modifier = primaryAxModifier
else if (props.currentSkills) modifier = props.currentSkills[0].modifier
if (modifier >= 0 && axOptions[modifier]) {
const primarySkill = axOptions[modifier]
if (primarySkill.secondary) {
const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>
{ax.name[locale]}
</option>
)
})
}
}
}
axOptionElements?.unshift(
<option key={-1} value={-1}>
{t('ax.no_skill')}
</option>
)
return axOptionElements
}
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const value = parseInt(event.target.value)
if (primaryAxModifierSelect.current == event.target) {
setPrimaryAxModifier(value)
if (
primaryAxValueInput.current &&
secondaryAxModifierSelect.current &&
secondaryAxValueInput.current
) {
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
secondaryAxModifierSelect.current.value = '-1'
secondaryAxValueInput.current.value = ''
}
} else {
setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
const currentAxSkill = primaryAxSkill.secondary
? primaryAxSkill.secondary.find((skill) => skill.id == value)
: undefined
if (secondaryAxValueInput.current)
setupInput(currentAxSkill, secondaryAxValueInput.current)
}
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value)
let newErrors = { ...errors }
if (primaryAxValueInput.current == event.target) {
if (handlePrimaryErrors(value)) setPrimaryAxValue(value)
} else {
if (handleSecondaryErrors(value)) setSecondaryAxValue(value)
}
}
function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = { ...errors }
if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = t('ax.errors.value_too_low', {
name: primaryAxSkill.name[locale],
minValue: primaryAxSkill.minValue,
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
})
} else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = t('ax.errors.value_too_high', {
name: primaryAxSkill.name[locale],
maxValue: primaryAxSkill.minValue,
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
})
} else if (!value || value <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill.name[locale],
})
} else {
newErrors.axValue1 = ''
}
setErrors(newErrors)
return newErrors.axValue1.length === 0
}
function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = { ...errors }
if (primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(
(skill) => skill.id == secondaryAxModifier
)
if (secondaryAxSkill) {
if (value < secondaryAxSkill.minValue) {
newErrors.axValue2 = t('ax.errors.value_too_low', {
name: secondaryAxSkill.name[locale],
minValue: secondaryAxSkill.minValue,
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
})
} else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = t('ax.errors.value_too_high', {
name: secondaryAxSkill.name[locale],
maxValue: secondaryAxSkill.minValue,
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
})
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = t('ax.errors.value_not_whole', {
name: secondaryAxSkill.name[locale],
})
} else if (primaryAxValue <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill.name[locale],
})
} else {
newErrors.axValue2 = ''
}
}
}
setErrors(newErrors)
return newErrors.axValue2.length === 0
}
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
if (ax) {
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`
element.disabled = false
element.placeholder = rangeString
element.min = `${ax.minValue}`
element.max = `${ax.maxValue}`
element.step = ax.suffix ? '0.5' : '1'
} else {
if (primaryAxValueInput.current && secondaryAxValueInput.current) {
if (primaryAxValueInput.current == element) {
primaryAxValueInput.current.disabled = true
primaryAxValueInput.current.placeholder = ''
}
secondaryAxValueInput.current.disabled = true
secondaryAxValueInput.current.placeholder = ''
}
}
}
return (
<div className="AXSelect">
<div className="AXSet">
<div className="fields">
<select
key="ax1"
defaultValue={
props.currentSkills && props.currentSkills[0]
? props.currentSkills[0].modifier
: -1
}
onChange={handleSelectChange}
ref={primaryAxModifierSelect}
>
{generateOptions(0)}
</select>
<input
defaultValue={
props.currentSkills && props.currentSkills[0]
? props.currentSkills[0].strength
: 0
}
className="Input"
type="number"
onChange={handleInputChange}
ref={primaryAxValueInput}
disabled={primaryAxValue != 0}
/>
</div>
<p className={primaryErrorClasses}>{errors.axValue1}</p>
</div>
<div className={secondarySetClasses}>
<div className="fields">
<select
key="ax2"
defaultValue={
props.currentSkills && props.currentSkills[1]
? props.currentSkills[1].modifier
: -1
}
onChange={handleSelectChange}
ref={secondaryAxModifierSelect}
>
{generateOptions(1)}
</select>
<input
defaultValue={
props.currentSkills && props.currentSkills[1]
? props.currentSkills[1].strength
: 0
}
className="Input"
type="number"
onChange={handleInputChange}
ref={secondaryAxValueInput}
disabled={secondaryAxValue != 0}
/>
</div>
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div>
</div>
)
} }
export default AXSelect export default AXSelect

View file

@ -1,214 +1,260 @@
.Button { .Button {
align-items: center; align-items: center;
background: var(--button-bg);
border: none;
border-radius: $input-corner;
color: var(--button-text);
display: inline-flex;
font-size: $font-button;
font-weight: $normal;
gap: 6px;
&:hover,
&.Blended:hover {
background: var(--button-bg-hover);
cursor: pointer;
color: var(--button-text-hover);
.Accessory svg {
fill: var(--button-text-hover);
}
.Accessory svg.stroke {
fill: none;
stroke: var(--button-text-hover);
}
}
&.Blended {
background: transparent; background: transparent;
border: none; }
border-radius: 6px;
color: $grey-50; &.Contained {
display: inline-flex; background: var(--button-contained-bg);
font-size: $font-button;
font-weight: $normal;
gap: 6px;
padding: 8px 12px;
&:hover { &:hover {
background: white; background: var(--button-contained-bg-hover);
cursor: pointer;
color: $grey-00;
.icon svg {
fill: $grey-00;
}
.icon.stroke svg {
fill: none;
stroke: $grey-00;
}
} }
&.destructive:hover { &.Save:hover .Accessory svg {
background: $error; fill: #ff4d4d;
color: white; stroke: #ff4d4d;
.icon svg {
fill: white;
}
} }
&.save:hover { &.Active.Save {
color: #FF4D4D; color: #ff4d4d;
.icon svg { .Accessory svg {
fill: #FF4D4D; fill: #ff4d4d;
stroke: #FF4D4D; stroke: #ff4d4d;
}
&:hover {
color: darken(#ff4d4d, 30);
.Accessory svg {
fill: darken(#ff4d4d, 30);
stroke: darken(#ff4d4d, 30);
} }
}
}
}
&.medium {
height: $unit * 5.5;
padding: ($unit * 1.5) $unit-2x;
}
&.small {
padding: $unit * 1.5;
}
&.destructive:hover {
background: $error;
color: $grey-100;
.Accessory svg {
fill: $grey-100;
}
}
&.save:hover {
color: #ff4d4d;
.Accessory svg {
fill: #ff4d4d;
stroke: #ff4d4d;
}
}
&.save.Active {
color: #ff4d4d;
&:hover {
color: darken(#ff4d4d, 30);
.icon svg {
fill: darken(#ff4d4d, 30);
stroke: darken(#ff4d4d, 30);
}
}
}
&.modal:hover {
background: $grey-90;
}
&.modal.destructive {
color: $error;
&:hover {
color: darken($error, 10);
}
}
.Accessory {
$dimension: $unit-2x;
display: flex;
svg {
fill: var(--button-text);
height: $dimension;
width: $dimension;
&.stroke {
fill: none;
stroke: var(--button-text);
}
&.Add {
height: 18px;
width: 18px;
}
&.Check {
height: 22px;
width: 22px;
}
} }
&.save.Active { &.check svg {
color: #FF4D4D; margin-top: 1px;
height: 14px;
.icon svg { width: auto;
fill: #FF4D4D;
stroke: #FF4D4D;
}
&:hover {
color: darken(#FF4D4D, 30);
.icon svg {
fill: darken(#FF4D4D, 30);
stroke: darken(#FF4D4D, 30);
}
}
} }
&.modal:hover { svg &.settings svg {
background: $grey-90; height: 13px;
width: 13px;
} }
}
&.modal.destructive { &.btn-blue {
color: $error; background: $blue;
color: #8b8b8b;
&:hover { &:hover {
color: darken($error, 10) background: #4b9be5;
} color: #233e56;
}
}
&.btn-red {
background: #fa4242;
color: #860f0f;
&:hover {
background: #e91a1a;
color: #4e1717;
.icon {
color: #4e1717;
}
} }
.icon { .icon {
margin-top: 2px; color: #860f0f;
svg {
fill: $grey-50;
height: 12px;
width: 12px;
}
&.check svg {
margin-top: 1px;
height: 14px;
width: auto;
}
&.stroke svg {
fill: none;
stroke: $grey-50;
}
&.settings svg {
height: 13px;
width: 13px;
}
} }
}
&.Active { &.btn-disabled {
background: white; background: #e0e0e0;
color: #bababa;
&:hover {
background: #e0e0e0;
color: #bababa;
} }
}
&.btn-blue { &.null {
background: $blue; background: $grey-90;
color: #8b8b8b; color: $grey-55;
&:hover { &:hover {
background: #4B9BE5; background: $grey-70;
color: #233E56; color: $grey-15;
}
} }
}
&.btn-red { &.wind {
background: #fa4242; background: $wind-bg-20;
color: #860f0f; color: $wind-text-10;
&:hover { &:hover {
background: #e91a1a; background: darken($wind-bg-20, 10);
color: #4e1717;
.icon {
color: #4e1717;
}
}
.icon {
color: #860f0f;
}
} }
}
&.btn-disabled { &.fire {
background: #e0e0e0; background: $fire-bg-20;
color: #bababa; color: $fire-text-10;
&:hover { &:hover {
background: #e0e0e0; background: darken($fire-bg-20, 10);
color: #bababa;
}
} }
}
&.null { &.water {
background: $grey-90; background: $water-bg-20;
color: $grey-50; color: $water-text-10;
&:hover { &:hover {
background: $grey-70; background: darken($water-bg-20, 10);
color: $grey-00;
}
} }
}
&.wind { &.earth {
background: $wind-bg-light; background: $earth-bg-20;
color: $wind-text-dark; color: $earth-text-10;
&:hover { &:hover {
background: darken($wind-bg-light, 10); background: darken($earth-bg-20, 10);
}
} }
}
&.fire { &.dark {
background: $fire-bg-light; background: $dark-bg-10;
color: $fire-text-dark; color: $dark-text-10;
&:hover { &:hover {
background: darken($fire-bg-light, 10); background: darken($dark-bg-10, 10);
}
} }
}
&.water { &.light {
background: $water-bg-light; background: $light-bg-20;
color: $water-text-dark; color: $light-text-10;
&:hover { &:hover {
background: darken($water-bg-light, 10); background: darken($light-bg-20, 10);
}
} }
}
&.earth { .Text {
background: $earth-bg-light; color: inherit;
color: $earth-text-dark; display: block;
width: 100%;
&:hover { }
background: darken($earth-bg-light, 10);
}
}
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
&:hover {
background: darken($dark-bg-light, 10);
}
}
&.light {
background: $light-bg-light;
color: $light-text-dark;
&:hover {
background: darken($light-bg-light, 10);
}
}
.text {
color: inherit;
display: block;
width: 100%;
}
} }

View file

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

View file

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

View file

@ -2,53 +2,56 @@ import React, { useEffect, useState } from 'react'
import './index.scss' import './index.scss'
interface Props { interface Props {
fieldName: string fieldName: string
placeholder: string placeholder: string
value?: string value?: string
limit: number limit: number
error: string error: string
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function useFieldSet(props, ref) { const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text' function useFieldSet(props, ref) {
const fieldType = ['password', 'confirm_password'].includes(props.fieldName)
? 'password'
: 'text'
const [currentCount, setCurrentCount] = useState(0) const [currentCount, setCurrentCount] = useState(0)
useEffect(() => { useEffect(() => {
setCurrentCount((props.value) ? props.limit - props.value.length : props.limit) setCurrentCount(
props.value ? props.limit - props.value.length : props.limit
)
}, [props.limit, props.value]) }, [props.limit, props.value])
function onChange(event: React.ChangeEvent<HTMLInputElement>) { function onChange(event: React.ChangeEvent<HTMLInputElement>) {
setCurrentCount(props.limit - event.currentTarget.value.length) setCurrentCount(props.limit - event.currentTarget.value.length)
if (props.onChange) props.onChange(event) if (props.onChange) props.onChange(event)
} }
return ( return (
<fieldset className="Fieldset"> <fieldset className="Fieldset">
<div className="Limited"> <div className="Limited">
<input <input
autoComplete="off" autoComplete="off"
className="Input" className="Input"
type={fieldType} type={fieldType}
name={props.fieldName} name={props.fieldName}
placeholder={props.placeholder} placeholder={props.placeholder}
defaultValue={props.value || ''} defaultValue={props.value || ''}
onBlur={props.onBlur} onBlur={props.onBlur}
onChange={onChange} onChange={onChange}
maxLength={props.limit} maxLength={props.limit}
ref={ref} ref={ref}
formNoValidate formNoValidate
/> />
<span className="Counter">{currentCount}</span> <span className="Counter">{currentCount}</span>
</div> </div>
{ {props.error.length > 0 && <p className="InputError">{props.error}</p>}
props.error.length > 0 && </fieldset>
<p className='InputError'>{props.error}</p>
}
</fieldset>
) )
}) }
)
export default CharLimitedFieldset export default CharLimitedFieldset

View file

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

View file

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

View file

@ -1,31 +1,31 @@
#CharacterGrid { #CharacterGrid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
margin: auto; margin: auto;
max-width: 761px; max-width: 761px;
} }
#grid_characters { #grid_characters {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
max-width: 761px; max-width: 761px;
@media (max-width: $medium-screen) {
justify-content: space-between;
width: 100%;
}
& > * {
margin-right: $unit * 3;
@media (max-width: $medium-screen) { @media (max-width: $medium-screen) {
justify-content: space-between; margin-right: inherit;
width: 100%;
} }
}
& > * { & > li:last-child {
margin-right: $unit * 3; margin: 0;
}
@media (max-width: $medium-screen) {
margin-right: inherit;
}
}
& > li:last-child {
margin: 0;
}
} }

View file

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

View file

@ -10,83 +10,114 @@ import UncapIndicator from '~components/UncapIndicator'
import './index.scss' import './index.scss'
interface Props { interface Props {
gridCharacter: GridCharacter gridCharacter: GridCharacter
children: React.ReactNode children: React.ReactNode
} }
interface KeyNames { interface KeyNames {
[key: string]: { [key: string]: {
en: string, en: string
jp: string jp: string
} }
} }
const CharacterHovercard = (props: Props) => { const CharacterHovercard = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = [
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const tintElement = Element[props.gridCharacter.object.element] const tintElement = Element[props.gridCharacter.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(' ', '_')}` const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(
' ',
'_'
)}`
function characterImage() { function characterImage() {
let imgSrc = "" let imgSrc = ''
if (props.gridCharacter) { if (props.gridCharacter) {
const character = props.gridCharacter.object const character = props.gridCharacter.object
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = '01' let suffix = '01'
if (props.gridCharacter.uncap_level == 6) if (props.gridCharacter.uncap_level == 6) suffix = '04'
suffix = '04' else if (props.gridCharacter.uncap_level == 5) suffix = '03'
else if (props.gridCharacter.uncap_level == 5) else if (props.gridCharacter.uncap_level > 2) suffix = '02'
suffix = '03'
else if (props.gridCharacter.uncap_level > 2)
suffix = '02'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
}
return imgSrc
} }
return ( return imgSrc
<HoverCard.Root> }
<HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard">
<div className="top">
<div className="title">
<h4>{ props.gridCharacter.object.name[locale] }</h4>
<img alt={props.gridCharacter.object.name[locale]} src={characterImage()} />
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon labelType={Element[props.gridCharacter.object.element]} />
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency1] } />
{ (props.gridCharacter.object.proficiency.proficiency2) ?
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency2] } />
: ''}
</div>
<UncapIndicator
type="character"
ulb={props.gridCharacter.object.uncap.ulb || false}
flb={props.gridCharacter.object.uncap.flb || false}
special={false}
/>
</div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a> return (
<HoverCard.Arrow /> <HoverCard.Root>
</HoverCard.Content> <HoverCard.Trigger>{props.children}</HoverCard.Trigger>
</HoverCard.Root> <HoverCard.Content className="Weapon Hovercard">
) <div className="top">
<div className="title">
<h4>{props.gridCharacter.object.name[locale]}</h4>
<img
alt={props.gridCharacter.object.name[locale]}
src={characterImage()}
/>
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon
labelType={Element[props.gridCharacter.object.element]}
/>
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency1
]
}
/>
{props.gridCharacter.object.proficiency.proficiency2 ? (
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency2
]
}
/>
) : (
''
)}
</div>
<UncapIndicator
type="character"
ulb={props.gridCharacter.object.uncap.ulb || false}
flb={props.gridCharacter.object.uncap.flb || false}
special={false}
/>
</div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
)
} }
export default CharacterHovercard export default CharacterHovercard

View file

@ -1,63 +1,67 @@
.CharacterResult { .CharacterResult {
border-radius: 6px;
display: flex;
gap: $unit;
padding: $unit * 1.5;
&:hover {
background: var(--button-contained-bg);
cursor: pointer;
.Info h5 {
color: var(--text-primary);
}
}
img {
background: var(--card-bg);
border-radius: 6px; border-radius: 6px;
display: inline-block;
height: 72px;
width: 120px;
}
.Info {
display: flex; display: flex;
gap: $unit; flex-direction: column;
padding: $unit * 1.5; flex-grow: 1;
gap: $unit-half;
&:hover { h5 {
background: $grey-90; color: var(--text-secondary);
cursor: pointer; display: inline-block;
font-size: $font-medium;
font-weight: $medium;
} }
img { .UncapIndicator {
background: $grey-80; justify-content: left;
border-radius: 6px; pointer-events: none;
display: inline-block;
height: 72px;
width: 120px;
} }
.Info { .stars {
display: flex; display: inline-block;
flex-direction: column; color: #ffa15e;
flex-grow: 1; font-size: $font-xlarge;
gap: calc($unit / 2);
h5 { & > span {
color: #555; color: #65daff;
display: inline-block; }
font-size: $font-medium;
font-weight: $medium;
}
.UncapIndicator {
justify-content: left;
pointer-events: none;
}
.stars {
display: inline-block;
color: #FFA15E;
font-size: $font-xlarge;
& > span {
color: #65DAFF;
}
}
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height/ $aspect-ratio);
}
}
} }
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height/ $aspect-ratio);
}
}
}
} }

View file

@ -7,45 +7,46 @@ import WeaponLabelIcon from '~components/WeaponLabelIcon'
import './index.scss' import './index.scss'
interface Props { interface Props {
data: Character data: Character
onClick: () => void onClick: () => void
} }
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const CharacterResult = (props: Props) => { const CharacterResult = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const character = props.data const character = props.data
const characterUrl = () => { const characterUrl = () => {
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg` let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`
if (character.granblue_id === '3030182000') { if (character.granblue_id === '3030182000') {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg` url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`
}
return url
} }
return ( return url
<li className="CharacterResult" onClick={props.onClick}> }
<img alt={character.name[locale]} src={characterUrl()} />
<div className="Info"> return (
<h5>{character.name[locale]}</h5> <li className="CharacterResult" onClick={props.onClick}>
<UncapIndicator <img alt={character.name[locale]} src={characterUrl()} />
type="character" <div className="Info">
flb={character.uncap.flb} <h5>{character.name[locale]}</h5>
ulb={character.uncap.ulb} <UncapIndicator
special={character.special} type="character"
/> flb={character.uncap.flb}
<div className="tags"> ulb={character.uncap.ulb}
<WeaponLabelIcon labelType={Element[character.element]} /> special={character.special}
</div> />
</div> <div className="tags">
</li> <WeaponLabelIcon labelType={Element[character.element]} />
) </div>
</div>
</li>
)
} }
export default CharacterResult export default CharacterResult

View file

@ -9,197 +9,260 @@ import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem' import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import './index.scss' import './index.scss'
import { emptyElementState, emptyProficiencyState, emptyRarityState } from '~utils/emptyStates' import {
emptyElementState,
emptyProficiencyState,
emptyRarityState,
} from '~utils/emptyStates'
import { elements, proficiencies, rarities } from '~utils/stateValues' import { elements, proficiencies, rarities } from '~utils/stateValues'
interface Props { interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void sendFilters: (filters: { [key: string]: number[] }) => void
} }
const CharacterSearchFilterBar = (props: Props) => { const CharacterSearchFilterBar = (props: Props) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [rarityMenu, setRarityMenu] = useState(false) const [rarityMenu, setRarityMenu] = useState(false)
const [elementMenu, setElementMenu] = useState(false) const [elementMenu, setElementMenu] = useState(false)
const [proficiency1Menu, setProficiency1Menu] = useState(false) const [proficiency1Menu, setProficiency1Menu] = useState(false)
const [proficiency2Menu, setProficiency2Menu] = useState(false) const [proficiency2Menu, setProficiency2Menu] = useState(false)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState) const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] = useState<ElementState>(emptyElementState) const [elementState, setElementState] =
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(emptyProficiencyState) useState<ElementState>(emptyElementState)
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(emptyProficiencyState) const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(
emptyProficiencyState
)
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(
emptyProficiencyState
)
function rarityMenuOpened(open: boolean) { function rarityMenuOpened(open: boolean) {
if (open) { if (open) {
setRarityMenu(true) setRarityMenu(true)
setElementMenu(false) setElementMenu(false)
setProficiency1Menu(false) setProficiency1Menu(false)
setProficiency2Menu(false) setProficiency2Menu(false)
} else setRarityMenu(false) } else setRarityMenu(false)
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(true)
setProficiency1Menu(false)
setProficiency2Menu(false)
} else setElementMenu(false)
}
function proficiency1MenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(false)
setProficiency1Menu(true)
setProficiency2Menu(false)
} else setProficiency1Menu(false)
}
function proficiency2MenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(false)
setProficiency1Menu(false)
setProficiency2Menu(true)
} else setProficiency2Menu(false)
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState)
newRarityState[key].checked = checked
setRarityState(newRarityState)
}
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState)
newElementState[key].checked = checked
setElementState(newElementState)
}
function handleProficiency1Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency1State)
newProficiencyState[key].checked = checked
setProficiency1State(newProficiencyState)
}
function handleProficiency2Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency2State)
newProficiencyState[key].checked = checked
setProficiency2State(newProficiencyState)
}
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState)
.filter((x) => x.checked)
.map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id)
const checkedProficiency1Filters = Object.values(proficiency1State)
.filter((x) => x.checked)
.map((x, i) => x.id)
const checkedProficiency2Filters = Object.values(proficiency2State)
.filter((x) => x.checked)
.map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiency1Filters,
proficiency2: checkedProficiency2Filters,
} }
function elementMenuOpened(open: boolean) { props.sendFilters(filters)
if (open) { }
setRarityMenu(false)
setElementMenu(true)
setProficiency1Menu(false)
setProficiency2Menu(false)
} else setElementMenu(false)
}
function proficiency1MenuOpened(open: boolean) { useEffect(() => {
if (open) { sendFilters()
setRarityMenu(false) }, [rarityState, elementState, proficiency1State, proficiency2State])
setElementMenu(false)
setProficiency1Menu(true)
setProficiency2Menu(false)
} else setProficiency1Menu(false)
}
function proficiency2MenuOpened(open: boolean) { function renderProficiencyFilter(proficiency: 1 | 2) {
if (open) { const onCheckedChange =
setRarityMenu(false) proficiency == 1 ? handleProficiency1Change : handleProficiency2Change
setElementMenu(false) const numSelected =
setProficiency1Menu(false) proficiency == 1
setProficiency2Menu(true) ? Object.values(proficiency1State)
} else setProficiency2Menu(false) .map((x) => x.checked)
} .filter(Boolean).length
: Object.values(proficiency2State)
function handleRarityChange(checked: boolean, key: string) { .map((x) => x.checked)
let newRarityState = cloneDeep(rarityState) .filter(Boolean).length
newRarityState[key].checked = checked const open = proficiency == 1 ? proficiency1Menu : proficiency2Menu
setRarityState(newRarityState) const onOpenChange =
} proficiency == 1 ? proficiency1MenuOpened : proficiency2MenuOpened
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState)
newElementState[key].checked = checked
setElementState(newElementState)
}
function handleProficiency1Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency1State)
newProficiencyState[key].checked = checked
setProficiency1State(newProficiencyState)
}
function handleProficiency2Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency2State)
newProficiencyState[key].checked = checked
setProficiency2State(newProficiencyState)
}
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
const checkedProficiency1Filters = Object.values(proficiency1State).filter(x => x.checked).map((x, i) => x.id)
const checkedProficiency2Filters = Object.values(proficiency2State).filter(x => x.checked).map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiency1Filters,
proficiency2: checkedProficiency2Filters
}
props.sendFilters(filters)
}
useEffect(() => {
sendFilters()
}, [rarityState, elementState, proficiency1State, proficiency2State])
function renderProficiencyFilter(proficiency: 1 | 2) {
const onCheckedChange = (proficiency == 1) ? handleProficiency1Change : handleProficiency2Change
const numSelected = (proficiency == 1)
? Object.values(proficiency1State).map(x => x.checked).filter(Boolean).length
: Object.values(proficiency2State).map(x => x.checked).filter(Boolean).length
const open = (proficiency == 1) ? proficiency1Menu : proficiency2Menu
const onOpenChange = (proficiency == 1) ? proficiency1MenuOpened : proficiency2MenuOpened
return (
<SearchFilter
label={`${t('filters.labels.proficiency')} ${proficiency}`}
numSelected={numSelected}
open={open}
onOpenChange={onOpenChange}>
<DropdownMenu.Label className="Label">{`${t('filters.labels.proficiency')} ${proficiency}`}</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked = (proficiency == 1)
? proficiency1State[proficiencies[i]].checked
: proficiency2State[proficiencies[i]].checked
return (
<SearchFilterCheckboxItem
key={proficiencies[i]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i]}>
{t(`proficiencies.${proficiencies[i]}`)}
</SearchFilterCheckboxItem>
)}
) }
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked = (proficiency == 1)
? proficiency1State[proficiencies[i + (proficiencies.length / 2)]].checked
: proficiency2State[proficiencies[i + (proficiencies.length / 2)]].checked
return (
<SearchFilterCheckboxItem
key={proficiencies[i + (proficiencies.length / 2)]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i + (proficiencies.length / 2)]}>
{t(`proficiencies.${proficiencies[i + (proficiencies.length / 2)]}`)}
</SearchFilterCheckboxItem>
)}
) }
</DropdownMenu.Group>
</section>
</SearchFilter>
)
}
return ( return (
<div className="SearchFilterBar"> <SearchFilter
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}> label={`${t('filters.labels.proficiency')} ${proficiency}`}
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label> numSelected={numSelected}
{ Array.from(Array(rarities.length)).map((x, i) => { open={open}
return ( onOpenChange={onOpenChange}
<SearchFilterCheckboxItem >
key={rarities[i]} <DropdownMenu.Label className="Label">{`${t(
onCheckedChange={handleRarityChange} 'filters.labels.proficiency'
checked={rarityState[rarities[i]].checked} )} ${proficiency}`}</DropdownMenu.Label>
valueKey={rarities[i]}> <section>
{t(`rarities.${rarities[i]}`)} <DropdownMenu.Group className="Group">
</SearchFilterCheckboxItem> {Array.from(Array(proficiencies.length / 2)).map((x, i) => {
)} const checked =
) } proficiency == 1
</SearchFilter> ? proficiency1State[proficiencies[i]].checked
: proficiency2State[proficiencies[i]].checked
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}> return (
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label> <SearchFilterCheckboxItem
{ Array.from(Array(elements.length)).map((x, i) => { key={proficiencies[i]}
return ( onCheckedChange={onCheckedChange}
<SearchFilterCheckboxItem checked={checked}
key={elements[i]} valueKey={proficiencies[i]}
onCheckedChange={handleElementChange} >
checked={elementState[elements[i]].checked} {t(`proficiencies.${proficiencies[i]}`)}
valueKey={elements[i]}> </SearchFilterCheckboxItem>
{t(`elements.${elements[i]}`)} )
</SearchFilterCheckboxItem> })}
)} </DropdownMenu.Group>
) } <DropdownMenu.Group className="Group">
</SearchFilter> {Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked =
proficiency == 1
? proficiency1State[
proficiencies[i + proficiencies.length / 2]
].checked
: proficiency2State[
proficiencies[i + proficiencies.length / 2]
].checked
{ renderProficiencyFilter(1) } return (
{ renderProficiencyFilter(2) } <SearchFilterCheckboxItem
</div> key={proficiencies[i + proficiencies.length / 2]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i + proficiencies.length / 2]}
>
{t(
`proficiencies.${
proficiencies[i + proficiencies.length / 2]
}`
)}
</SearchFilterCheckboxItem>
)
})}
</DropdownMenu.Group>
</section>
</SearchFilter>
) )
}
return (
<div className="SearchFilterBar">
<SearchFilter
label={t('filters.labels.rarity')}
numSelected={
Object.values(rarityState)
.map((x) => x.checked)
.filter(Boolean).length
}
open={rarityMenu}
onOpenChange={rarityMenuOpened}
>
<DropdownMenu.Label className="Label">
{t('filters.labels.rarity')}
</DropdownMenu.Label>
{Array.from(Array(rarities.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
key={rarities[i]}
onCheckedChange={handleRarityChange}
checked={rarityState[rarities[i]].checked}
valueKey={rarities[i]}
>
{t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem>
)
})}
</SearchFilter>
<SearchFilter
label={t('filters.labels.element')}
numSelected={
Object.values(elementState)
.map((x) => x.checked)
.filter(Boolean).length
}
open={elementMenu}
onOpenChange={elementMenuOpened}
>
<DropdownMenu.Label className="Label">
{t('filters.labels.element')}
</DropdownMenu.Label>
{Array.from(Array(elements.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
key={elements[i]}
onCheckedChange={handleElementChange}
checked={elementState[elements[i]].checked}
valueKey={elements[i]}
>
{t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem>
)
})}
</SearchFilter>
{renderProficiencyFilter(1)}
{renderProficiencyFilter(2)}
</div>
)
} }
export default CharacterSearchFilterBar export default CharacterSearchFilterBar

View file

@ -1,79 +1,78 @@
.CharacterUnit { .CharacterUnit {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
min-height: 320px;
max-width: 200px;
margin-bottom: $unit * 4;
&.editable .CharacterImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-tall;
}
&.filled h3 {
display: block;
}
&.filled ul {
display: flex; display: flex;
flex-direction: column; }
gap: calc($unit / 2);
min-height: 320px;
max-width: 200px;
margin-bottom: $unit * 4;
&.editable .CharacterImage:hover { h3,
border: $hover-stroke; ul {
box-shadow: $hover-shadow; display: none;
cursor: pointer; }
transform: $scale-tall;
h3 {
color: var(--text-primary);
font-size: $font-regular;
font-weight: $normal;
line-height: 1.1;
margin: 0;
max-width: 131px;
text-align: center;
word-wrap: normal;
}
img {
position: relative;
width: 100%;
z-index: 2;
}
.CharacterImage {
aspect-ratio: 131 / 273;
background: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.18s ease-in-out;
height: auto;
width: 131px;
@media (max-width: $medium-screen) {
width: 17vw;
} }
&.filled h3 { &:hover .icon svg {
display: block; fill: var(--icon-secondary-hover);
} }
&.filled ul { .icon {
display: flex; position: absolute;
} height: $unit * 3;
width: $unit * 3;
h3, z-index: 1;
ul {
display: none; svg {
} fill: var(--icon-secondary);
}
h3 {
color: #333;
font-size: $font-regular;
font-weight: $normal;
line-height: 1.1;
margin: 0;
max-width: 131px;
text-align: center;
word-wrap: normal;
}
img {
position: relative;
width: 100%;
z-index: 2;
}
.CharacterImage {
aspect-ratio: 131 / 273;
background: white;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.18s ease-in-out;
height: auto;
width: 131px;
@media (max-width: $medium-screen) {
width: 17vw;
}
&:hover .icon svg {
color: $grey-40;
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
svg {
fill: $grey-70;
}
}
} }
}
} }

View file

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

View file

@ -0,0 +1,87 @@
.Dialog {
$multiplier: 4;
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
openModal;
background: var(--dialog-bg);
border-radius: $card-corner;
display: flex;
flex-direction: column;
gap: $unit * $multiplier;
height: auto;
min-width: $unit * 48;
min-height: $unit-12x;
padding: $unit * $multiplier;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 21;
.DialogHeader {
display: flex;
align-items: center;
gap: $unit;
.left {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit;
p {
font-size: $font-small;
line-height: 1.25;
}
}
}
.DialogClose {
background: transparent;
&:hover {
cursor: pointer;
svg {
fill: $error;
}
}
svg {
fill: $grey-50;
float: right;
height: 24px;
width: 24px;
}
}
.DialogTitle {
color: var(--text-primary);
font-size: $font-xlarge;
flex-grow: 1;
}
.DialogTop {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
.SubTitle {
color: var(--text-secondary);
font-size: $font-small;
font-weight: $medium;
}
}
.DialogDescription {
color: var(--text-secondary);
flex-grow: 1;
}
.actions {
display: flex;
justify-content: flex-end;
width: 100%;
}
}

View file

@ -0,0 +1,39 @@
import React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import classNames from 'classnames'
import './index.scss'
interface Props
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {}
export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
function dialog({ children, ...props }, forwardedRef) {
const classes = classNames(
{
Dialog: true,
},
props.className
)
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="Overlay" />
<DialogPrimitive.Content
className={classes}
{...props}
ref={forwardedRef}
>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
}
)
export const Dialog = DialogPrimitive.Root
export const DialogTrigger = DialogPrimitive.Trigger
export const DialogClose = DialogPrimitive.Close

View file

@ -1,64 +1,65 @@
.ToggleGroup { .ToggleGroup {
$height: 36px; $height: 36px;
border: 1px solid rgba(0, 0, 0, 0.14); border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: $height; border-radius: $height;
display: flex; display: flex;
height: $height; height: $height;
gap: calc($unit / 4); gap: calc($unit / 4);
padding: calc($unit / 2); padding: calc($unit / 2);
.ToggleItem { .ToggleItem {
background: white; background: $grey-100;
border: none; border: none;
border-radius: 18px; border-radius: 18px;
color: $grey-40; color: $grey-50;
flex-grow: 1; flex-grow: 1;
font-size: $font-regular; font-size: $font-regular;
padding: ($unit) $unit * 2; padding: ($unit) $unit * 2;
&.ja { &.ja {
padding-top: 6px; padding-top: 6px;
padding-bottom: 10px; padding-bottom: 10px;
}
&:hover {
cursor: pointer;
}
&:hover, &[data-state="on"] {
background:$grey-80;
color: $grey-00;
&.fire {
background: $fire-bg-light;
color: $fire-text-dark;
}
&.water {
background: $water-bg-light;
color: $water-text-dark;
}
&.earth {
background: $earth-bg-light;
color: $earth-text-dark;
}
&.wind {
background: $wind-bg-light;
color: $wind-text-dark;
}
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
}
&.light {
background: $light-bg-light;
color: $light-text-dark;
}
}
} }
&:hover {
cursor: pointer;
}
&:hover,
&[data-state='on'] {
background: $grey-80;
color: $grey-15;
&.fire {
background: $fire-bg-20;
color: $fire-text-10;
}
&.water {
background: $water-bg-20;
color: $water-text-10;
}
&.earth {
background: $earth-bg-20;
color: $earth-text-10;
}
&.wind {
background: $wind-bg-20;
color: $wind-text-10;
}
&.dark {
background: $dark-bg-10;
color: $dark-text-10;
}
&.light {
background: $light-bg-20;
color: $light-text-10;
}
}
}
} }

View file

@ -7,40 +7,75 @@ import * as ToggleGroup from '@radix-ui/react-toggle-group'
import './index.scss' import './index.scss'
interface Props { interface Props {
currentElement: number currentElement: number
sendValue: (value: string) => void sendValue: (value: string) => void
} }
const ElementToggle = (props: Props) => { const ElementToggle = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
return ( return (
<ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}> <ToggleGroup.Root
<ToggleGroup.Item className={`ToggleItem ${locale}`} value="0" aria-label="null"> className="ToggleGroup"
{t('elements.null')} type="single"
</ToggleGroup.Item> defaultValue={`${props.currentElement}`}
<ToggleGroup.Item className={`ToggleItem wind ${locale}`} value="1" aria-label="wind"> aria-label="Element"
{t('elements.wind')} onValueChange={props.sendValue}
</ToggleGroup.Item> >
<ToggleGroup.Item className={`ToggleItem fire ${locale}`} value="2" aria-label="fire"> <ToggleGroup.Item
{t('elements.fire')} className={`ToggleItem ${locale}`}
</ToggleGroup.Item> value="0"
<ToggleGroup.Item className={`ToggleItem water ${locale}`} value="3" aria-label="water"> aria-label="null"
{t('elements.water')} >
</ToggleGroup.Item> {t('elements.null')}
<ToggleGroup.Item className={`ToggleItem earth ${locale}`} value="4" aria-label="earth"> </ToggleGroup.Item>
{t('elements.earth')} <ToggleGroup.Item
</ToggleGroup.Item> className={`ToggleItem wind ${locale}`}
<ToggleGroup.Item className={`ToggleItem dark ${locale}`} value="5" aria-label="dark"> value="1"
{t('elements.dark')} aria-label="wind"
</ToggleGroup.Item> >
<ToggleGroup.Item className={`ToggleItem light ${locale}`} value="6" aria-label="light"> {t('elements.wind')}
{t('elements.light')} </ToggleGroup.Item>
</ToggleGroup.Item> <ToggleGroup.Item
</ToggleGroup.Root> className={`ToggleItem fire ${locale}`}
) value="2"
aria-label="fire"
>
{t('elements.fire')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem water ${locale}`}
value="3"
aria-label="water"
>
{t('elements.water')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem earth ${locale}`}
value="4"
aria-label="earth"
>
{t('elements.earth')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem dark ${locale}`}
value="5"
aria-label="dark"
>
{t('elements.dark')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem light ${locale}`}
value="6"
aria-label="light"
>
{t('elements.light')}
</ToggleGroup.Item>
</ToggleGroup.Root>
)
} }
export default ElementToggle export default ElementToggle

View file

@ -1,55 +1,55 @@
#ExtraSummons { #ExtraSummons {
background: #FFEBD9; background: var(--subaura-orange-bg);
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
display: flex;
justify-content: center;
margin: 20px auto;
max-width: 727px;
padding: 16px 16px 16px 0;
position: relative;
left: 9px;
@media (max-width: $medium-screen) {
left: auto;
max-width: auto;
width: 100%;
}
& > span {
color: var(--subaura-orange-text);
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
margin: 20px auto; line-height: 1.2;
max-width: 727px; font-weight: 500;
padding: 16px 16px 16px 0; margin-right: 16px;
position: relative; text-align: center;
left: 9px; width: 387px;
}
@media (max-width: $medium-screen) { #grid_summons {
left: auto; display: grid;
max-width: auto; grid-template-columns: auto auto;
width: 100%; grid-column-gap: $unit * 2;
grid-template-rows: 1fr;
grid-row-gap: $unit * 3;
& > li {
list-style: none;
min-height: 0;
.SummonUnit {
min-height: 0;
}
} }
}
& > span { .SummonUnit .SummonImage {
color: #825B39; background: var(--subaura-orange-card-bg);
display: flex; }
align-items: center;
justify-content: center;
line-height: 1.2;
font-weight: 500;
margin-right: 16px;
text-align: center;
width: 387px;
}
#grid_summons { .SummonUnit .SummonImage .icon svg {
display: grid; fill: var(--subaura-orange-secondary);
grid-template-columns: auto auto; }
grid-column-gap: $unit * 2;
grid-template-rows: 1fr;
grid-row-gap: $unit * 3;
& > li {
list-style: none;
min-height: 0;
.SummonUnit {
min-height: 0;
}
}
}
.SummonUnit .SummonImage {
background: #facea7;
}
.SummonUnit .SummonImage .icon svg {
fill: #a8703f;
}
} }

View file

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

View file

@ -1,47 +1,47 @@
#ExtraGrid { #ExtraGrid {
background: #ECEBFF; background: var(--extra-purple-bg);
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
display: flex;
justify-content: center;
margin: 20px auto;
max-width: 766px;
padding: 16px 16px 16px 0;
position: relative;
left: 8px;
@media (max-width: $medium-screen) {
left: auto;
max-width: auto;
width: 100%;
}
& > span {
color: var(--extra-purple-text);
display: flex; display: flex;
align-items: center;
flex-grow: 1;
justify-content: center; justify-content: center;
margin: 20px auto; line-height: 1.2;
max-width: 766px; font-weight: 500;
padding: 16px 16px 16px 0; margin-right: 16px;
position: relative; text-align: center;
left: 8px; }
@media (max-width: $medium-screen) { .grid_weapons {
left: auto; display: flex;
max-width: auto; flex-direction: row;
width: 100%; flex-wrap: wrap;
} margin: 0;
padding: 0;
max-width: 528px;
}
& > span { .WeaponUnit .WeaponImage {
color: #4F3C79; background: var(--extra-purple-card-bg);
display: flex; }
align-items: center;
flex-grow: 1;
justify-content: center;
line-height: 1.2;
font-weight: 500;
margin-right: 16px;
text-align: center;
}
.grid_weapons { .WeaponUnit .WeaponImage .icon svg {
display: flex; fill: var(--extra-purple-secondary);
flex-direction: row; }
flex-wrap: wrap;
margin: 0;
padding: 0;
max-width: 528px;
}
.WeaponUnit .WeaponImage {
background: #D5D3F6;
}
.WeaponUnit .WeaponImage .icon svg {
fill: #8F8AC6;
}
} }

View file

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

View file

@ -1,33 +0,0 @@
.Fieldset {
border: none;
display: inline-flex;
flex-direction: column;
padding: 0;
margin: 0 0 $unit 0;
.Input {
-webkit-font-smoothing: antialiased;
border: none;
background-color: white;
border-radius: 6px;
box-sizing: border-box;
color: $grey-00;
display: block;
font-size: $font-regular;
padding: 12px 16px;
width: 100%;
}
.InputError {
color: $error;
font-size: $font-tiny;
margin: $unit 0;
padding: calc($unit / 2) ($unit * 2);
}
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #a9a9a9 !important;
opacity: 1; /* Firefox */
}

View file

@ -1,38 +0,0 @@
import React from 'react'
import './index.scss'
interface Props {
fieldName: string
placeholder: string
value?: string
error: string
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const Fieldset = React.forwardRef<HTMLInputElement, Props>(function fieldSet(props, ref) {
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text'
return (
<fieldset className="Fieldset">
<input
autoComplete="off"
className="Input"
type={fieldType}
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ''}
onBlur={props.onBlur}
onChange={props.onChange}
ref={ref}
formNoValidate
/>
{
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset>
)
})
export default Fieldset

View file

@ -1,63 +1,76 @@
.FilterBar { .FilterBar {
align-items: center;
background: var(--bar-bg);
border-radius: 6px;
display: flex;
flex-direction: row;
gap: $unit * 2;
margin: 0 auto;
margin-top: 7px; // Line up with HeaderMenu
padding: $unit * 2;
position: sticky;
transition: box-shadow 0.24s ease-in-out;
top: $unit * 4;
width: 966px;
&.shadow {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
}
h1 {
color: var(--text-primary);
font-size: $font-regular;
font-weight: $normal;
flex-grow: 1;
text-align: left;
}
select,
.SelectTrigger {
// background: url("/icons/Arrow.svg"), $grey-90;
// background-repeat: no-repeat;
// background-position-y: center;
// background-position-x: 95%;
// background-size: $unit * 1.5;
background-color: var(--select-contained-bg);
color: $grey-55;
font-size: $font-small;
margin: 0;
max-width: 200px;
&:hover {
background-color: var(--select-contained-bg-hover);
}
}
.SelectTrigger {
width: 100%;
span {
font-size: $font-small;
}
}
.UserInfo {
align-items: center; align-items: center;
background: white;
border-radius: 6px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: $unit * 2; flex-grow: 1;
margin: 0 auto; gap: $unit * 1.5;
margin-top: 7px; // Line up with HeaderMenu
padding: $unit * 2;
position: sticky;
transition: box-shadow 0.24s ease-in-out;
top: $unit * 4;
width: 966px;
&.shadow { img {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14); $diameter: $unit * 6;
} border-radius: $diameter / 2;
height: $diameter;
h1 { width: $diameter;
color: $grey-20;
font-size: $font-regular; &.gran {
font-weight: $normal; background-color: #cee7fe;
flex-grow: 1; }
text-align: left;
} &.djeeta {
background-color: #ffe1fe;
select { }
background: url('/icons/Arrow.svg'), $grey-90;
background-repeat: no-repeat;
background-position-y: center;
background-position-x: 95%;
background-size: $unit * 1.5;
color: $grey-50;
font-size: $font-small;
margin: 0;
max-width: 200px;
}
.UserInfo {
align-items: center;
display: flex;
flex-direction: row;
flex-grow: 1;
gap: $unit * 1.5;
img {
$diameter: $unit * 6;
border-radius: $diameter / 2;
height: $diameter;
width: $diameter;
&.gran {
background-color: #CEE7FE;
}
&.djeeta {
background-color: #FFE1FE;
}
}
} }
}
} }

View file

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

View file

@ -1,148 +1,147 @@
.GridRep { .GridRep {
border-radius: 6px; border-radius: 6px;
display: flex;
flex-direction: column;
gap: $unit;
padding: $unit * 2;
&:hover {
background: var(--grid-rep-hover);
h2,
.Grid {
cursor: pointer;
}
.Grid .weapon {
box-shadow: inset 0 0 0 1px var(--grid-border-color);
}
}
.Grid {
display: flex;
flex-direction: row;
flex-shrink: 0;
.weapon {
background: var(--card-bg);
border-radius: 4px;
}
.grid_mainhand {
margin-right: $unit;
height: 139px;
width: 66px;
}
.grid_weapons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: $unit;
margin: 0;
padding: 0;
width: fit-content;
}
.grid_weapon {
float: left;
height: 40px;
width: 70px;
}
.grid_mainhand img[src*='jpg'],
.grid_weapon img[src*='jpg'] {
border-radius: 4px;
width: 100%;
height: 100%;
}
}
.Details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit; gap: calc($unit / 2);
padding: $unit * 2;
&:hover { h2 {
background: white; color: var(--text-primary);
font-size: $font-regular;
overflow: hidden;
padding-bottom: 1px;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 258px; // Can we not do this?
h2, .Grid { &.empty {
cursor: pointer; color: var(--text-tertiary);
} }
.Grid .weapon {
box-shadow: inset 0 0 0 1px $grey-80;
}
} }
.Grid { .top {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-shrink: 0; gap: calc($unit / 2);
align-items: center;
.weapon { .info {
background: white;
border-radius: 4px;
}
.grid_mainhand {
margin-right: $unit;
height: 139px;
width: 66px;
}
.grid_weapons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: $unit;
margin: 0;
padding: 0;
width: fit-content;
}
.grid_weapon {
float: left;
height: 40px;
width: 70px;
}
.grid_mainhand img[src*="jpg"],
.grid_weapon img[src*="jpg"] {
border-radius: 4px;
width: 100%;
height: 100%;
}
}
.Details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2); gap: calc($unit / 2);
}
h2 { button svg {
color: $grey-00; width: 14px;
font-size: $font-regular; height: 14px;
overflow: hidden; }
padding-bottom: 1px;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 258px; // Can we not do this?
&.empty {
color: $grey-50;
}
}
.top {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
align-items: center;
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
}
button svg {
width: 14px;
height: 14px;
}
button:hover,
button.Active {
background: $grey-90;
}
}
.bottom {
display: flex;
flex-direction: row;
}
.raid, .user, time {
color: $grey-50;
font-size: $font-small;
}
.raid, .user {
flex-grow: 1;
}
.raid {
margin-bottom: calc($unit / 2);
}
.user {
display: flex;
gap: calc($unit / 2);
align-items: center;
img, .no-user {
$diameter: 18px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
img.gran {
background-color: #CEE7FE;
}
img.djeeta {
background-color: #FFE1FE;
}
.no-user {
background: $grey-80;
}
}
} }
.bottom {
display: flex;
flex-direction: row;
}
.raid,
.user,
time {
color: $grey-55;
font-size: $font-small;
}
.raid,
.user {
flex-grow: 1;
}
.raid {
margin-bottom: calc($unit / 2);
}
.user {
display: flex;
gap: calc($unit / 2);
align-items: center;
img,
.no-user {
$diameter: 18px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
img.gran {
background-color: #cee7fe;
}
img.djeeta {
background-color: #ffe1fe;
}
.no-user {
background: $grey-80;
}
}
}
} }

View file

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

View file

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

View file

@ -1,37 +1,37 @@
.Header { .Header {
display: flex;
margin-bottom: $unit-2x;
width: 100%;
&.bottom {
position: sticky;
bottom: $unit * 2;
}
#right > div {
display: flex; display: flex;
height: 34px; gap: 8px;
width: 100%; }
&.bottom { .dropdown {
position: sticky; display: inline-block;
bottom: $unit * 2; position: relative;
&:hover {
padding-right: 50px;
.Button {
background: var(--button-bg-hover);
color: var(--button-text-hover);
}
.Menu {
display: block;
}
} }
}
#right > div { .push {
display: flex; margin-left: auto;
gap: 8px; }
}
.dropdown {
display: inline-block;
position: relative;
&:hover {
padding-right: 50px;
padding-bottom: 16px;
.Button {
background: white;
}
.Menu {
display: block;
}
}
}
.push {
margin-left: auto;
}
} }

View file

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

View file

@ -1,147 +1,150 @@
.Menu { .Menu {
background: white; background: var(--menu-bg);
border-radius: 6px; border-radius: 6px;
display: none; display: none;
min-width: 220px; min-width: 220px;
position: absolute; position: absolute;
top: $unit * 5; // This shouldn't be hardcoded. How to calculate it? top: $unit * 5; // This shouldn't be hardcoded. How to calculate it?
z-index: 10; // Also, add space that doesn't make the menu disappear if you move your mouse slowly
z-index: 10;
} }
.MenuItem { .MenuItem {
color: $grey-40; color: var(--text-tertiary);
font-weight: $normal; font-weight: $normal;
&:hover:not(.disabled) { &:hover:not(.disabled) {
background: $grey-100; background: var(--menu-bg-item-hover);
color: $grey-00; color: var(--text-primary);
cursor: pointer; cursor: pointer;
a {
color: $grey-00;
}
}
&.profile > div {
padding: 6px 12px;
}
&.language {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
padding-right: $unit;
span {
flex-grow: 1;
}
.Switch {
$height: 24px;
background: $grey-60;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 44px;
height: $height;
&:hover {
cursor: pointer;
}
.Thumb {
$diameter: 18px;
background: white;
border-radius: calc($diameter / 2);
display: block;
height: $diameter;
width: $diameter;
transition: transform 100ms;
transform: translateX(-2px);
z-index: 3;
&:hover {
cursor: pointer;
}
&[data-state="checked"] {
background: white;
transform: translateX(17px);
}
}
.left, .right {
color: white;
font-size: 10px;
font-weight: $bold;
position: absolute;
z-index: 2;
}
.left {
top: 6px;
left: 6px;
}
.right {
top: 6px;
right: 5px;
}
}
}
a { a {
color: $grey-40; color: var(--text-primary);
}
}
&.profile > div {
padding: 6px 12px;
}
&.language {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
padding-right: $unit;
span {
flex-grow: 1;
} }
& > a, & > span { .Switch {
$height: 24px;
background: $grey-60;
border-radius: calc($height / 2);
border: none;
position: relative;
width: 44px;
height: $height;
&:hover {
cursor: pointer;
}
.Thumb {
$diameter: 18px;
background: $grey-100;
border-radius: calc($diameter / 2);
display: block; display: block;
padding: 12px 12px; height: $diameter;
} width: $diameter;
transition: transform 100ms;
& > div { transform: translateX(-2px);
align-items: center; z-index: 3;
display: flex;
flex-direction: row;
padding: 10px 12px;
&:hover { &:hover {
i.tag { cursor: pointer;
background: $grey-60;
color: white;
}
} }
span { &[data-state='checked'] {
flex-grow: 1; background: $grey-100;
transform: translateX(17px);
} }
}
img { .left,
$diameter: 32px; .right {
border-radius: calc($diameter / 2); color: $grey-100;
height: $diameter; font-size: 10px;
width: $diameter; font-weight: $bold;
} position: absolute;
z-index: 2;
}
.left {
top: 6px;
left: 6px;
}
.right {
top: 6px;
right: 5px;
}
} }
}
a {
color: $grey-50;
}
& > a,
& > span {
display: block;
padding: 12px 12px;
}
& > div {
align-items: center;
display: flex;
flex-direction: row;
padding: 10px 12px;
&:hover {
i.tag {
background: var(--tag-bg);
color: var(--tag-text);
}
}
span {
flex-grow: 1;
}
img {
$diameter: 32px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
}
} }
.MenuGroup { .MenuGroup {
border-bottom: 1px solid #f5f5f5; border-bottom: 1px solid var(--menu-separator);
&:first-child .MenuItem:first-child:hover { &:first-child .MenuItem:first-child:hover {
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-top-right-radius: 6px; border-top-right-radius: 6px;
} }
&:last-child .MenuItem:last-child:hover { &:last-child .MenuItem:last-child:hover {
border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;
} }
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
} }

View file

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

View file

@ -0,0 +1,23 @@
.Input {
-webkit-font-smoothing: antialiased;
background-color: var(--input-bg);
border: none;
border-radius: 6px;
box-sizing: border-box;
display: block;
padding: $unit-2x;
width: 100%;
}
.InputError {
color: $error;
font-size: $font-tiny;
margin: $unit 0;
padding: calc($unit / 2) ($unit * 2);
}
::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--text-secondary) !important;
opacity: 1; /* Firefox */
}

View file

@ -0,0 +1,38 @@
import classNames from 'classnames'
import React from 'react'
import './index.scss'
interface Props
extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
error?: string
label?: string
}
const Input = React.forwardRef<HTMLInputElement, Props>(function input(
props: Props,
forwardedRef
) {
const classes = classNames({ Input: true }, props.className)
return (
<label className="Label" htmlFor={props.name}>
<input
{...props}
autoComplete="off"
className={classes}
defaultValue={props.value || ''}
ref={forwardedRef}
formNoValidate
/>
{props.label}
{props.error && props.error.length > 0 && (
<p className="InputError">{props.error}</p>
)}
</label>
)
})
export default Input

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,10 @@ import type { ReactElement } from 'react'
import TopHeader from '~components/TopHeader' import TopHeader from '~components/TopHeader'
interface Props { interface Props {
children: ReactElement children: ReactElement
} }
const Layout = ({children}: Props) => { const Layout = ({ children }: Props) => {
return ( return (
<> <>
<TopHeader /> <TopHeader />

View file

@ -1,31 +1,31 @@
.Login.Dialog form { .Login.Dialog form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc($unit / 2); gap: calc($unit / 2);
margin-bottom: $unit; margin-bottom: $unit;
.Button { .Button {
font-size: $font-regular; font-size: $font-regular;
padding: ($unit * 1.5) ($unit * 2); padding: ($unit * 1.5) ($unit * 2);
width: 100%; width: 100%;
&.btn-disabled { &.btn-disabled {
background: $grey-90; background: $grey-90;
color: $grey-70; color: $grey-70;
cursor: not-allowed; cursor: not-allowed;
}
&:not(.btn-disabled) {
background: $grey-90;
color: $grey-40;
&:hover {
background: $grey-80;
}
}
} }
input { &:not(.btn-disabled) {
background: $grey-90; background: $grey-90;
color: $grey-50;
&:hover {
background: $grey-80;
}
} }
}
input {
background: $grey-90;
}
} }

View file

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

View file

@ -1,7 +1,7 @@
#Party .Extra { #Party .Extra {
color: #888; color: #888;
display: flex; display: flex;
font-weight: 500; font-weight: 500;
gap: 8px; gap: 8px;
line-height: 34px; line-height: 34px;
} }

View file

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

View file

@ -1,153 +1,159 @@
.PartyDetails { .PartyDetails {
display: none; // This breaks transition, find a workaround display: none; // This breaks transition, find a workaround
opacity: 0; opacity: 0;
margin: 0 auto; margin: $unit-4x auto 0;
margin-bottom: 100px; max-width: $unit * 94;
max-width: $unit * 95; position: relative;
position: relative;
&.Editable { &.Editable {
top: $unit; top: $unit;
height: 0; height: 0;
z-index: 2; z-index: 2;
transition: opacity 0.2s ease-in-out, transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
top 0.2s ease-in-out;
&.Visible {
&.Visible { display: flex;
display: block; flex-direction: column;
height: auto; gap: $unit;
margin-bottom: 40vh; height: auto;
opacity: 1; opacity: 1;
top: 0; top: 0;
}
fieldset {
display: block;
width: 100%;
textarea {
min-height: $unit * 20;
width: 100%;
}
}
.bottom {
display: flex;
flex-direction: row;
gap: $unit;
.left {
flex-grow: 1;
}
.right {
display: flex;
flex-direction: row;
gap: $unit;
}
}
} }
&.ReadOnly { fieldset {
top: $unit * -1; display: block;
transition: opacity 0.2s ease-in-out, width: 100%;
top 0.2s ease-in-out;
&.Visible { textarea {
display: block; min-height: $unit * 20;
height: auto; width: 100%;
opacity: 1; }
top: 0; }
}
a:hover { .bottom {
text-decoration: underline; display: flex;
} flex-direction: row;
gap: $unit;
margin-bottom: $unit-12x;
p { .left {
font-size: $font-regular; flex-grow: 1;
line-height: $font-regular * 1.2; }
white-space: pre-line;
}
.right {
display: flex;
flex-direction: row;
gap: $unit;
}
}
}
&.ReadOnly {
top: $unit * -1;
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
&.Visible {
display: block;
height: auto;
opacity: 1;
top: 0;
}
a:hover {
text-decoration: underline;
}
p {
font-size: $font-regular;
line-height: $font-regular * 1.2;
white-space: pre-line;
}
h1 {
font-size: $font-xlarge;
font-weight: $normal;
text-align: left;
margin-bottom: $unit;
}
.info {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
margin-bottom: $unit * 2;
.left {
flex-grow: 1;
h1 { h1 {
font-size: $font-xlarge; color: var(--text-primary);
font-weight: $normal;
text-align: left; &.empty {
margin-bottom: $unit; color: var(--text-secondary);
} }
.info {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
margin-bottom: $unit * 2;
.left {
flex-grow: 1;
}
}
.attribution {
align-items: center;
display: flex;
flex-direction: row;
& > div {
align-items: center;
display: inline-flex;
font-size: $font-small;
height: 26px;
}
time {
font-size: $font-small;
}
& > *:not(:last-child):after {
content: " · ";
margin: 0 calc($unit / 2);
}
}
.user {
align-items: center;
display: inline-flex;
gap: calc($unit / 2);
margin-top: 1px;
img, .no-user {
$diameter: 24px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
img.gran {
background-color: #CEE7FE;
}
img.djeeta {
background-color: #FFE1FE;
}
.no-user {
background: $grey-80;
}
} }
}
} }
.attribution {
align-items: center;
display: flex;
flex-direction: row;
& > div {
align-items: center;
display: inline-flex;
font-size: $font-small;
height: 26px;
}
time {
font-size: $font-small;
}
& > *:not(:last-child):after {
content: ' · ';
margin: 0 calc($unit / 2);
}
}
.user {
align-items: center;
display: inline-flex;
gap: calc($unit / 2);
margin-top: 1px;
img,
.no-user {
$diameter: 24px;
border-radius: calc($diameter / 2);
height: $diameter;
width: $diameter;
}
img.gran {
background-color: #cee7fe;
}
img.djeeta {
background-color: #ffe1fe;
}
.no-user {
background: $grey-80;
}
}
}
} }
.EmptyDetails { .EmptyDetails {
display: none; display: none;
justify-content: center; justify-content: center;
margin-bottom: $unit * 10; margin: $unit-4x 0 $unit-10x;
&.Visible { &.Visible {
display: flex; display: flex;
} }
} }

View file

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

View file

@ -1,20 +1,20 @@
.PartyNavigation { .PartyNavigation {
display: flex; display: flex;
gap: 58px; gap: 58px;
justify-content: center; justify-content: center;
margin: 0 auto; margin: 0 auto;
margin-bottom: $unit * 3; margin-bottom: $unit * 3;
max-width: 760px; max-width: 760px;
position: relative; position: relative;
} }
.ExtraSwitch { .ExtraSwitch {
color: #888; color: #888;
display: flex; display: flex;
font-weight: $normal; font-weight: $normal;
gap: 8px; gap: 8px;
line-height: 34px; line-height: 34px;
height: 100%; height: 100%;
position: absolute; position: absolute;
right: 0px; right: 0px;
} }

View file

@ -10,89 +10,104 @@ import ToggleSwitch from '~components/ToggleSwitch'
import { GridType } from '~utils/enums' import { GridType } from '~utils/enums'
import './index.scss' import './index.scss'
interface Props { interface Props {
selectedTab: GridType selectedTab: GridType
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
onCheckboxChange: (event: React.ChangeEvent<HTMLInputElement>) => void onCheckboxChange: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
const PartySegmentedControl = (props: Props) => { const PartySegmentedControl = (props: Props) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState)
function getElement() { function getElement() {
let element: number = 0 let element: number = 0
if (party.element == 0 && grid.weapons.mainWeapon) if (party.element == 0 && grid.weapons.mainWeapon)
element = grid.weapons.mainWeapon.element element = grid.weapons.mainWeapon.element
else else element = party.element
element = party.element
switch(element) { switch (element) {
case 1: return "wind"; break case 1:
case 2: return "fire"; break return 'wind'
case 3: return "water"; break break
case 4: return "earth"; break case 2:
case 5: return "dark"; break return 'fire'
case 6: return "light"; break break
} case 3:
return 'water'
break
case 4:
return 'earth'
break
case 5:
return 'dark'
break
case 6:
return 'light'
break
} }
}
const extraToggle = const extraToggle = (
<div className="ExtraSwitch"> <div className="ExtraSwitch">
Extra Extra
<ToggleSwitch <ToggleSwitch
name="ExtraSwitch" name="ExtraSwitch"
editable={party.editable} editable={party.editable}
checked={party.extra} checked={party.extra}
onChange={props.onCheckboxChange} onChange={props.onCheckboxChange}
/> />
</div> </div>
)
return ( return (
<div className="PartyNavigation"> <div className="PartyNavigation">
<SegmentedControl elementClass={getElement()}> <SegmentedControl elementClass={getElement()}>
{/* <Segment {/* <Segment
groupName="grid" groupName="grid"
name="class" name="class"
selected={props.selectedTab === GridType.Class} selected={props.selectedTab === GridType.Class}
onClick={props.onClick} onClick={props.onClick}
>Class</Segment> */} >Class</Segment> */}
<Segment <Segment
groupName="grid" groupName="grid"
name="characters" name="characters"
selected={props.selectedTab == GridType.Character} selected={props.selectedTab == GridType.Character}
onClick={props.onClick} onClick={props.onClick}
>{t('party.segmented_control.characters')}</Segment> >
{t('party.segmented_control.characters')}
</Segment>
<Segment <Segment
groupName="grid" groupName="grid"
name="weapons" name="weapons"
selected={props.selectedTab == GridType.Weapon} selected={props.selectedTab == GridType.Weapon}
onClick={props.onClick} onClick={props.onClick}
>{t('party.segmented_control.weapons')}</Segment> >
{t('party.segmented_control.weapons')}
</Segment>
<Segment <Segment
groupName="grid" groupName="grid"
name="summons" name="summons"
selected={props.selectedTab == GridType.Summon} selected={props.selectedTab == GridType.Summon}
onClick={props.onClick} onClick={props.onClick}
>{t('party.segmented_control.summons')}</Segment> >
</SegmentedControl> {t('party.segmented_control.summons')}
</Segment>
</SegmentedControl>
{ {(() => {
(() => { if (party.editable && props.selectedTab == GridType.Weapon) {
if (party.editable && props.selectedTab == GridType.Weapon) { return extraToggle
return extraToggle }
} })()}
})() </div>
} )
</div>
)
} }
export default PartySegmentedControl export default PartySegmentedControl

View file

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

View file

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

View file

@ -6,29 +6,29 @@ import ArrowIcon from '~public/icons/Arrow.svg'
import './index.scss' import './index.scss'
interface Props { interface Props {
label: string label: string
open: boolean open: boolean
numSelected: number numSelected: number
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
children: React.ReactNode children: React.ReactNode
} }
const SearchFilter = (props: Props) => { const SearchFilter = (props: Props) => {
return ( return (
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}> <DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
<DropdownMenu.Trigger className="DropdownLabel"> <DropdownMenu.Trigger className="DropdownLabel">
{props.label} {props.label}
<span className="count">{props.numSelected}</span> <span className="count">{props.numSelected}</span>
<span className="icon"> <span className="icon">
<ArrowIcon /> <ArrowIcon />
</span> </span>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content className="Dropdown" sideOffset={4}> <DropdownMenu.Content className="Dropdown" sideOffset={4}>
{props.children} {props.children}
<DropdownMenu.Arrow /> <DropdownMenu.Arrow />
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
) )
} }
export default SearchFilter export default SearchFilter

View file

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

View file

@ -6,29 +6,30 @@ import CheckIcon from '~public/icons/Check.svg'
import './index.scss' import './index.scss'
interface Props { interface Props {
checked?: boolean checked?: boolean
valueKey: string valueKey: string
onCheckedChange: (open: boolean, key: string) => void onCheckedChange: (open: boolean, key: string) => void
children: React.ReactNode children: React.ReactNode
} }
const SearchFilterCheckboxItem = (props: Props) => { const SearchFilterCheckboxItem = (props: Props) => {
function handleCheckedChange(checked: boolean) { function handleCheckedChange(checked: boolean) {
props.onCheckedChange(checked, props.valueKey) props.onCheckedChange(checked, props.valueKey)
} }
return ( return (
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
className="Item" className="Item"
checked={props.checked || false} checked={props.checked || false}
onCheckedChange={handleCheckedChange} onCheckedChange={handleCheckedChange}
onSelect={ (event) => event.preventDefault() }> onSelect={(event) => event.preventDefault()}
<DropdownMenu.ItemIndicator className="Indicator"> >
<CheckIcon /> <DropdownMenu.ItemIndicator className="Indicator">
</DropdownMenu.ItemIndicator> <CheckIcon />
{props.children} </DropdownMenu.ItemIndicator>
</DropdownMenu.CheckboxItem> {props.children}
) </DropdownMenu.CheckboxItem>
)
} }
export default SearchFilterCheckboxItem export default SearchFilterCheckboxItem

View file

@ -1,98 +1,99 @@
.Search.Dialog { .Search.Dialog {
display: flex;
flex-direction: column;
min-height: 431px;
width: 600px;
height: 480px;
gap: 0;
padding: 0;
#Header {
border-bottom: 1px solid transparent;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 431px; gap: $unit;
width: 600px; padding-bottom: $unit * 2;
height: 480px;
gap: 0;
padding: 0;
#Header { &.scrolled {
border-bottom: 1px solid transparent; border-bottom: 1px solid rgba(0, 0, 0, 0.1);
display: flex; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
flex-direction: column;
gap: $unit;
padding-bottom: $unit * 2;
&.scrolled {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
}
#Bar {
border-top-left-radius: $unit;
border-top-right-radius: $unit;
display: flex;
gap: $unit * 2.5;
margin: 0;
padding: ($unit * 3) ($unit * 3) 0 ($unit * 3);
position: sticky;
top: 0;
button {
background: transparent;
border: none;
height: 42px;
padding: 0;
}
label {
width: 100%;
.Input {
background: $grey-90;
border: none;
border-radius: calc($unit / 2);
box-sizing: border-box;
font-size: $font-regular;
padding: $unit * 1.5;
text-align: left;
width: 100%;
}
}
}
} }
#Results { #Bar {
margin: 0; align-items: center;
max-height: 356px; border-top-left-radius: $unit;
padding: 0 ($unit * 1.5); border-top-right-radius: $unit;
overflow-y: scroll; display: flex;
gap: $unit * 2.5;
margin: 0;
padding: ($unit * 3) ($unit * 3) 0 ($unit * 3);
position: sticky;
top: 0;
h5.total { button {
font-size: $font-regular; background: transparent;
font-weight: $normal; border: none;
color: $grey-40; height: 42px;
padding: calc($unit / 2) ($unit * 1.5); padding: 0;
} }
.footer { label {
align-items: center; width: 100%;
display: flex;
color: $grey-60;
font-size: $font-regular;
font-weight: $normal;
height: $unit * 10;
justify-content: center;
}
.WeaponResult:last-child { // .Input {
margin-bottom: $unit * 1.5; // background: $grey-90;
} // border: none;
// border-radius: calc($unit / 2);
// box-sizing: border-box;
// font-size: $font-regular;
// padding: $unit * 1.5;
// text-align: left;
// width: 100%;
// }
}
} }
}
#Results {
margin: 0;
max-height: 356px;
padding: 0 ($unit * 1.5);
overflow-y: scroll;
h5.total {
font-size: $font-regular;
font-weight: $normal;
color: var(--text-tertiary);
padding: $unit-half ($unit * 1.5);
}
.footer {
align-items: center;
display: flex;
color: var(--text-tertiary);
font-size: $font-regular;
font-weight: $normal;
height: $unit-10x;
justify-content: center;
}
.WeaponResult:last-child {
margin-bottom: $unit * 1.5;
}
}
} }
.Search.Dialog #NoResults { .Search.Dialog #NoResults {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-grow: 1; flex-grow: 1;
} }
.Search.Dialog #NoResults h2 { .Search.Dialog #NoResults h2 {
color: #ccc; color: var(--text-secondary);
font-size: $font-large; font-size: $font-large;
font-weight: 500; font-weight: 500;
margin-top: -32px; margin-top: $unit-4x * -1;
} }

View file

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

View file

@ -1,36 +1,36 @@
.Segment { .Segment {
color: $grey-50; color: $grey-55;
cursor: pointer;
font-size: 1.4rem;
font-weight: $normal;
min-width: 100px;
&:hover label {
background: var(--page-hover);
color: var(--text-primary);
}
& input {
display: none;
&:checked + label {
background: var(--background);
color: var(--text-primary);
}
}
& label {
border-radius: $unit * 3;
display: block;
text-align: center;
white-space: nowrap;
overflow: hidden;
padding: 8px 12px;
text-overflow: ellipsis;
cursor: pointer; cursor: pointer;
font-size: 1.4rem;
font-weight: $normal;
min-width: 100px;
&:hover label { &:before {
background: $grey-90; background: #fff;
color: $grey-40;
}
& input {
display: none;
&:checked + label {
background: $grey-90;
color: $grey-00;
}
}
& label {
border-radius: $unit * 3;
display: block;
text-align: center;
white-space: nowrap;
overflow: hidden;
padding: 8px 12px;
text-overflow: ellipsis;
cursor: pointer;
&:before {
background: #fff;
}
} }
}
} }

View file

@ -3,31 +3,27 @@ import React from 'react'
import './index.scss' import './index.scss'
interface Props { interface Props {
groupName: string groupName: string
name: string name: string
selected: boolean selected: boolean
children: string children: string
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
const Segment: React.FC<Props> = (props: Props) => { const Segment: React.FC<Props> = (props: Props) => {
return (
<div className="Segment">
return ( <input
<div className="Segment"> name={props.groupName}
<input id={props.name}
name={props.groupName} value={props.name}
id={props.name} type="radio"
value={props.name} checked={props.selected}
type="radio" onChange={props.onClick}
checked={props.selected} />
onChange={props.onClick} <label htmlFor={props.name}>{props.children}</label>
/> </div>
<label htmlFor={props.name}> )
{props.children}
</label>
</div>
)
} }
export default Segment export default Segment

View file

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

View file

@ -3,17 +3,17 @@ import React from 'react'
import './index.scss' import './index.scss'
interface Props { interface Props {
elementClass?: string elementClass?: string
} }
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => { const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
return ( return (
<div className="SegmentedControlWrapper"> <div className="SegmentedControlWrapper">
<div className={`SegmentedControl ${(elementClass) ? elementClass : ''}`}> <div className={`SegmentedControl ${elementClass ? elementClass : ''}`}>
{children} {children}
</div> </div>
</div> </div>
) )
} }
export default SegmentedControl export default SegmentedControl

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

@ -1,47 +1,47 @@
.Signup.Dialog form { .Signup.Dialog form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc($unit / 2); gap: calc($unit / 2);
margin-bottom: $unit; margin-bottom: $unit;
.Button { .Button {
font-size: $font-regular; font-size: $font-regular;
padding: ($unit * 1.5) ($unit * 2); padding: ($unit * 1.5) ($unit * 2);
width: 100%; width: 100%;
&.btn-disabled { &.btn-disabled {
background: $grey-90; background: $grey-90;
color: $grey-70; color: $grey-70;
cursor: not-allowed; cursor: not-allowed;
}
&:not(.btn-disabled) {
background: $grey-90;
color: $grey-40;
&:hover {
background: $grey-80;
}
}
} }
.terms { &:not(.btn-disabled) {
color: $grey-40; background: $grey-90;
font-size: $font-small; color: $grey-50;
line-height: 1.2;
margin-top: $unit;
text-align: center;
a { &:hover {
color: $blue; background: $grey-80;
}
&:hover {
color: darken($blue, 30);
}
}
} }
}
input { .terms {
background: $grey-90; color: $grey-50;
font-size: $font-small;
line-height: 1.2;
margin-top: $unit;
text-align: center;
a {
color: $blue;
&:hover {
color: darken($blue, 30);
}
} }
}
input {
background: $grey-90;
}
} }

View file

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

View file

@ -1,26 +1,26 @@
#SummonGrid { #SummonGrid {
display: grid;
grid-template-columns: auto auto auto;
grid-column-gap: $unit * 2;
justify-content: center;
& .Label {
color: $grey-55;
font-size: $font-tiny;
font-weight: $medium;
margin-bottom: $unit;
text-align: center;
}
#grid_summons {
display: grid; display: grid;
grid-template-columns: auto auto auto; grid-template-columns: auto auto;
grid-column-gap: $unit * 2; grid-column-gap: $unit * 2;
justify-content: center; grid-template-rows: 1fr;
grid-row-gap: $unit * 3;
& .Label { & > li {
color: $grey-50; list-style: none;
font-size: $font-tiny;
font-weight: $medium;
margin-bottom: $unit;
text-align: center;
}
#grid_summons {
display: grid;
grid-template-columns: auto auto;
grid-column-gap: $unit * 2;
grid-template-rows: 1fr;
grid-row-gap: $unit * 3;
& > li {
list-style: none;
}
} }
}
} }

View file

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

View file

@ -10,71 +10,88 @@ import UncapIndicator from '~components/UncapIndicator'
import './index.scss' import './index.scss'
interface Props { interface Props {
gridSummon: GridSummon gridSummon: GridSummon
children: React.ReactNode children: React.ReactNode
} }
const SummonHovercard = (props: Props) => { const SummonHovercard = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const tintElement = Element[props.gridSummon.object.element] const tintElement = Element[props.gridSummon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}` const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(
' ',
'_'
)}`
function summonImage() { function summonImage() {
let imgSrc = "" let imgSrc = ''
if (props.gridSummon) { if (props.gridSummon) {
const summon = props.gridSummon.object const summon = props.gridSummon.object
const upgradedSummons = [ const upgradedSummons = [
'2040094000', '2040100000', '2040080000', '2040098000', '2040094000',
'2040090000', '2040084000', '2040003000', '2040056000' '2040100000',
] '2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
]
let suffix = '' let suffix = ''
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5) if (
suffix = '_02' upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5
)
suffix = '_02'
// Generate the correct source for the summon // Generate the correct source for the summon
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
}
return imgSrc
} }
return ( return imgSrc
<HoverCard.Root> }
<HoverCard.Trigger>
{ props.children } return (
</HoverCard.Trigger> <HoverCard.Root>
<HoverCard.Content className="Weapon Hovercard"> <HoverCard.Trigger>{props.children}</HoverCard.Trigger>
<div className="top"> <HoverCard.Content className="Weapon Hovercard">
<div className="title"> <div className="top">
<h4>{ props.gridSummon.object.name[locale] }</h4> <div className="title">
<img alt={props.gridSummon.object.name[locale]} src={summonImage()} /> <h4>{props.gridSummon.object.name[locale]}</h4>
</div> <img
<div className="subInfo"> alt={props.gridSummon.object.name[locale]}
<div className="icons"> src={summonImage()}
<WeaponLabelIcon labelType={Element[props.gridSummon.object.element]}/> />
</div> </div>
<UncapIndicator <div className="subInfo">
type="summon" <div className="icons">
ulb={props.gridSummon.object.uncap.ulb || false} <WeaponLabelIcon
flb={props.gridSummon.object.uncap.flb || false} labelType={Element[props.gridSummon.object.element]}
special={false} />
/> </div>
</div> <UncapIndicator
</div> type="summon"
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a> ulb={props.gridSummon.object.uncap.ulb || false}
<HoverCard.Arrow /> flb={props.gridSummon.object.uncap.flb || false}
</HoverCard.Content> special={false}
</HoverCard.Root> />
) </div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
)
} }
export default SummonHovercard export default SummonHovercard

View file

@ -1,63 +1,67 @@
.SummonResult { .SummonResult {
border-radius: 6px;
display: flex;
gap: $unit;
padding: $unit * 1.5;
&:hover {
background: var(--button-contained-bg);
cursor: pointer;
.Info h5 {
color: var(--text-primary);
}
}
img {
background: $grey-80;
border-radius: 6px; border-radius: 6px;
display: inline-block;
height: auto;
width: 120px;
}
.Info {
display: flex; display: flex;
gap: $unit; flex-direction: column;
padding: $unit * 1.5; flex-grow: 1;
gap: $unit-half;
&:hover { h5 {
background: $grey-90; color: var(--text-secondary);
cursor: pointer; display: inline-block;
font-size: $font-medium;
font-weight: $medium;
} }
img { .UncapIndicator {
background: $grey-80; justify-content: left;
border-radius: 6px; pointer-events: none;
display: inline-block;
height: auto;
width: 120px;
} }
.Info { .stars {
display: flex; display: inline-block;
flex-direction: column; color: #ffa15e;
flex-grow: 1; font-size: $font-xlarge;
gap: calc($unit / 2);
h5 { & > span {
color: #555; color: #65daff;
display: inline-block; }
font-size: $font-medium;
font-weight: $medium;
}
.UncapIndicator {
justify-content: left;
pointer-events: none;
}
.stars {
display: inline-block;
color: #FFA15E;
font-size: $font-xlarge;
& > span {
color: #65DAFF;
}
}
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height / $aspect-ratio);
}
}
} }
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height / $aspect-ratio);
}
}
}
} }

View file

@ -7,35 +7,39 @@ import WeaponLabelIcon from '~components/WeaponLabelIcon'
import './index.scss' import './index.scss'
interface Props { interface Props {
data: Summon data: Summon
onClick: () => void onClick: () => void
} }
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const SummonResult = (props: Props) => { const SummonResult = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const summon = props.data const summon = props.data
return ( return (
<li className="SummonResult" onClick={props.onClick}> <li className="SummonResult" onClick={props.onClick}>
<img alt={summon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`} /> <img
<div className="Info"> alt={summon.name[locale]}
<h5>{summon.name[locale]}</h5> src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}.jpg`}
<UncapIndicator />
type="summon" <div className="Info">
flb={summon.uncap.flb} <h5>{summon.name[locale]}</h5>
ulb={summon.uncap.ulb} <UncapIndicator
special={false} type="summon"
/> flb={summon.uncap.flb}
<div className="tags"> ulb={summon.uncap.ulb}
<WeaponLabelIcon labelType={Element[summon.element]} /> special={false}
</div> />
</div> <div className="tags">
</li> <WeaponLabelIcon labelType={Element[summon.element]} />
) </div>
</div>
</li>
)
} }
export default SummonResult export default SummonResult

View file

@ -13,93 +13,122 @@ import { emptyElementState, emptyRarityState } from '~utils/emptyStates'
import { elements, rarities } from '~utils/stateValues' import { elements, rarities } from '~utils/stateValues'
interface Props { interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void sendFilters: (filters: { [key: string]: number[] }) => void
} }
const SummonSearchFilterBar = (props: Props) => { const SummonSearchFilterBar = (props: Props) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [rarityMenu, setRarityMenu] = useState(false) const [rarityMenu, setRarityMenu] = useState(false)
const [elementMenu, setElementMenu] = useState(false) const [elementMenu, setElementMenu] = useState(false)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState) const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] = useState<ElementState>(emptyElementState) const [elementState, setElementState] =
useState<ElementState>(emptyElementState)
function rarityMenuOpened(open: boolean) { function rarityMenuOpened(open: boolean) {
if (open) { if (open) {
setRarityMenu(true) setRarityMenu(true)
setElementMenu(false) setElementMenu(false)
} else setRarityMenu(false) } else setRarityMenu(false)
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(true)
} else setElementMenu(false)
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState)
newRarityState[key].checked = checked
setRarityState(newRarityState)
}
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState)
newElementState[key].checked = checked
setElementState(newElementState)
}
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState)
.filter((x) => x.checked)
.map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
} }
function elementMenuOpened(open: boolean) { props.sendFilters(filters)
if (open) { }
setRarityMenu(false)
setElementMenu(true)
} else setElementMenu(false)
}
function handleRarityChange(checked: boolean, key: string) { useEffect(() => {
let newRarityState = cloneDeep(rarityState) sendFilters()
newRarityState[key].checked = checked }, [rarityState, elementState])
setRarityState(newRarityState)
}
function handleElementChange(checked: boolean, key: string) { return (
let newElementState = cloneDeep(elementState) <div className="SearchFilterBar">
newElementState[key].checked = checked <SearchFilter
setElementState(newElementState) label={t('filters.labels.rarity')}
} numSelected={
Object.values(rarityState)
function sendFilters() { .map((x) => x.checked)
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id) .filter(Boolean).length
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters
} }
open={rarityMenu}
onOpenChange={rarityMenuOpened}
>
<DropdownMenu.Label className="Label">
{t('filters.labels.rarity')}
</DropdownMenu.Label>
{Array.from(Array(rarities.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
key={rarities[i]}
onCheckedChange={handleRarityChange}
checked={rarityState[rarities[i]].checked}
valueKey={rarities[i]}
>
{t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem>
)
})}
</SearchFilter>
props.sendFilters(filters) <SearchFilter
} label={t('filters.labels.element')}
numSelected={
useEffect(() => { Object.values(elementState)
sendFilters() .map((x) => x.checked)
}, [rarityState, elementState]) .filter(Boolean).length
}
return ( open={elementMenu}
<div className="SearchFilterBar"> onOpenChange={elementMenuOpened}
<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> <DropdownMenu.Label className="Label">
{ Array.from(Array(rarities.length)).map((x, i) => { {t('filters.labels.element')}
return ( </DropdownMenu.Label>
<SearchFilterCheckboxItem {Array.from(Array(elements.length)).map((x, i) => {
key={rarities[i]} return (
onCheckedChange={handleRarityChange} <SearchFilterCheckboxItem
checked={rarityState[rarities[i]].checked} key={elements[i]}
valueKey={rarities[i]}> onCheckedChange={handleElementChange}
{t(`rarities.${rarities[i]}`)} checked={elementState[elements[i]].checked}
</SearchFilterCheckboxItem> valueKey={elements[i]}
)} >
) } {t(`elements.${elements[i]}`)}
</SearchFilter> </SearchFilterCheckboxItem>
)
<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> </SearchFilter>
{ Array.from(Array(elements.length)).map((x, i) => { </div>
return ( )
<SearchFilterCheckboxItem
key={elements[i]}
onCheckedChange={handleElementChange}
checked={elementState[elements[i]].checked}
valueKey={elements[i]}>
{t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem>
)}
) }
</SearchFilter>
</div>
)
} }
export default SummonSearchFilterBar export default SummonSearchFilterBar

View file

@ -1,106 +1,107 @@
.SummonUnit { .SummonUnit {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
&.main .SummonImage, &.main .SummonImage,
&.friend .SummonImage { &.friend .SummonImage {
aspect-ratio: 182 / 315; aspect-ratio: 182 / 315;
width: 182px; width: 182px;
height: auto; height: auto;
@media (max-width: $medium-screen) { @media (max-width: $medium-screen) {
width: 20.3vw; width: 20.3vw;
}
} }
}
&.grid { &.grid {
// max-width: 148px; // max-width: 148px;
// min-height: 141px; // min-height: 141px;
min-height: 180px; min-height: 180px;
@media (max-width: $medium-screen) { @media (max-width: $medium-screen) {
min-height: 16.5vw; min-height: 16.5vw;
}
.SummonImage {
aspect-ratio: 148 / 111;
list-style-type: none;
width: 148px;
height: auto;
@media (max-width: $medium-screen) {
width: 20vw;
}
}
}
&.friend {
margin-right: 0;
}
&.main.editable .SummonImage:hover,
&.friend.editable .SummonImage:hover {
transform: $scale-tall;
}
&.editable .SummonImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
} }
.SummonImage { .SummonImage {
background: white; aspect-ratio: 148 / 111;
border: 1px solid rgba(0, 0, 0, 0); list-style-type: none;
border-radius: $unit; width: 148px;
display: flex; height: auto;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.18s ease-in-out;
&:hover .icon svg { @media (max-width: $medium-screen) {
fill: $grey-40; width: 20vw;
} }
}
}
.icon { &.friend {
position: absolute; margin-right: 0;
height: $unit * 3; }
width: $unit * 3;
z-index: 1;
svg { &.main.editable .SummonImage:hover,
fill: $grey-70; &.friend.editable .SummonImage:hover {
} transform: $scale-tall;
} }
&.editable .SummonImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
}
.SummonImage {
background: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.18s ease-in-out;
&:hover .icon svg {
fill: var(--icon-secondary-hover);
} }
&.filled h3 { .icon {
display: block; position: absolute;
} height: $unit * 3;
width: $unit * 3;
z-index: 1;
&.filled ul { svg {
display: flex; fill: var(--icon-secondary);
}
} }
}
h3, ul { &.filled h3 {
display: none; display: block;
} }
h3 { &.filled ul {
color: #333; display: flex;
font-size: $font-regular; }
font-weight: $normal;
line-height: 1.1;
margin: 0;
text-align: center;
}
img { h3,
position: relative; ul {
width: 100%; display: none;
z-index: 2; }
}
h3 {
color: var(--text-primary);
font-size: $font-regular;
font-weight: $normal;
line-height: 1.1;
margin: 0;
text-align: center;
}
img {
position: relative;
width: 100%;
z-index: 2;
}
} }

View file

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

View file

@ -1,5 +1,18 @@
.Fieldset textarea { .Fieldset textarea {
color: $grey-00; $offset: 2px;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 21px; 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

@ -2,32 +2,31 @@ import React from 'react'
import './index.scss' import './index.scss'
interface Props { interface Props {
fieldName: string fieldName: string
placeholder: string placeholder: string
value?: string value?: string
error: string error: string
onBlur?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void onBlur?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
} }
const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(function fieldSet(props, ref) { const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(
function fieldSet(props, ref) {
return ( return (
<fieldset className="Fieldset"> <fieldset className="Fieldset">
<textarea <textarea
className="Input" className="Input"
name={props.fieldName} name={props.fieldName}
placeholder={props.placeholder} placeholder={props.placeholder}
defaultValue={props.value || ''} defaultValue={props.value || ''}
onBlur={props.onBlur} onBlur={props.onBlur}
onChange={props.onChange} onChange={props.onChange}
ref={ref} ref={ref}
/> />
{ {props.error.length > 0 && <p className="InputError">{props.error}</p>}
props.error.length > 0 && </fieldset>
<p className='InputError'>{props.error}</p>
}
</fieldset>
) )
}) }
)
export default TextFieldset export default TextFieldset

View file

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

View file

@ -3,29 +3,29 @@ import React from 'react'
import './index.scss' import './index.scss'
interface Props { interface Props {
name: string name: string
checked: boolean checked: boolean
editable: boolean editable: boolean
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
const ToggleSwitch: React.FC<Props> = (props: Props) => { const ToggleSwitch: React.FC<Props> = (props: Props) => {
return ( return (
<div className="toggle-switch"> <div className="toggle-switch">
<input <input
className="toggle-switch-checkbox" className="toggle-switch-checkbox"
name={props.name} name={props.name}
id={props.name} id={props.name}
type="checkbox" type="checkbox"
checked={props.checked} checked={props.checked}
disabled={!props.editable} disabled={!props.editable}
onChange={props.onChange} onChange={props.onChange}
/> />
<label className="toggle-switch-label" htmlFor={props.name}> <label className="toggle-switch-label" htmlFor={props.name}>
<span className="toggle-switch-switch" /> <span className="toggle-switch-switch" />
</label> </label>
</div> </div>
) )
} }
export default ToggleSwitch export default ToggleSwitch

View file

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

View file

@ -1,14 +1,14 @@
.UncapIndicator { .UncapIndicator {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-content: center; align-content: center;
justify-content: center; justify-content: center;
gap: 2px; gap: 2px;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
} }

View file

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

View file

@ -1,55 +1,55 @@
.UncapStar { .UncapStar {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 18px 18px; background-size: 18px 18px;
display: block; display: block;
height: 18px; height: 18px;
width: 18px; width: 18px;
&:hover {
transform: scale(1.2);
}
&.empty,
&.empty.mlb,
&.empty.flb,
&.empty.ulb,
&.empty.special {
background: url('/icons/uncap/empty.svg');
&:hover { &:hover {
transform: scale(1.2); background: url('/icons/uncap/empty-hover.svg');
} }
}
&.empty, &.mlb {
&.empty.mlb, background: url('/icons/uncap/yellow.svg');
&.empty.flb,
&.empty.ulb,
&.empty.special {
background: url('/icons/uncap/empty.svg');
&:hover { &:hover {
background: url('/icons/uncap/empty-hover.svg'); background: url('/icons/uncap/yellow-hover.svg');
}
} }
}
&.mlb { &.special {
background: url('/icons/uncap/yellow.svg'); background: url('/icons/uncap/red.svg');
&:hover { &:hover {
background: url('/icons/uncap/yellow-hover.svg'); background: url('/icons/uncap/red-hover.svg');
}
} }
}
&.special { &.flb {
background: url('/icons/uncap/red.svg'); background: url('/icons/uncap/blue.svg');
&:hover { &:hover {
background: url('/icons/uncap/red-hover.svg'); background: url('/icons/uncap/blue-hover.svg');
}
} }
}
&.flb { &.ulb {
background: url('/icons/uncap/blue.svg'); background: url('/icons/uncap/purple.svg');
&:hover { &:hover {
background: url('/icons/uncap/blue-hover.svg'); background: url('/icons/uncap/purple-hover.svg');
}
}
&.ulb {
background: url('/icons/uncap/purple.svg');
&:hover {
background: url('/icons/uncap/purple-hover.svg');
}
} }
}
} }

View file

@ -4,39 +4,36 @@ import classnames from 'classnames'
import './index.scss' import './index.scss'
interface Props { interface Props {
empty: boolean empty: boolean
special: boolean special: boolean
flb: boolean flb: boolean
ulb: boolean ulb: boolean
index: number index: number
onClick: (index: number, empty: boolean) => void onClick: (index: number, empty: boolean) => void
} }
const UncapStar = (props: Props) => { const UncapStar = (props: Props) => {
const classes = classnames({ const classes = classnames({
UncapStar: true, UncapStar: true,
'empty': props.empty, empty: props.empty,
'special': props.special, special: props.special,
'mlb': !props.special, mlb: !props.special,
'flb': props.flb, flb: props.flb,
'ulb': props.ulb ulb: props.ulb,
})
}) function clicked() {
props.onClick(props.index, props.empty)
}
function clicked() { return <li className={classes} onClick={clicked}></li>
props.onClick(props.index, props.empty)
}
return (
<li className={classes} onClick={clicked}></li>
)
} }
UncapStar.defaultProps = { UncapStar.defaultProps = {
empty: false, empty: false,
special: false, special: false,
flb: false, flb: false,
ulb: false ulb: false,
} }
export default UncapStar export default UncapStar

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