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 {
width: $unit * 60;
width: $unit * 60;
section {
margin-bottom: $unit;
section {
margin-bottom: $unit;
h2 {
margin-bottom: $unit * 3;
}
h2 {
margin-bottom: $unit * 3;
}
}
.DialogDescription {
font-size: $font-regular;
line-height: 1.24;
margin-bottom: $unit;
.DialogDescription {
font-size: $font-regular;
line-height: 1.24;
margin-bottom: $unit;
&:last-of-type {
margin-bottom: 0;
}
&:last-of-type {
margin-bottom: 0;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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