Remove trailing semicolons

This commit is contained in:
Justin Edmund 2022-12-05 17:54:46 -08:00
parent 6d6eaf59ee
commit e8843699c7
106 changed files with 3690 additions and 3731 deletions

View file

@ -1,6 +1,5 @@
{
"editor.formatOnSave": true,
"prettier.semi": false,
"semi": false,
"tabWidth": 2,
"singleQuote": true
}

View file

@ -1,18 +1,18 @@
import React from "react";
import { useTranslation } from "next-i18next";
import * as Dialog from "@radix-ui/react-dialog";
import React from 'react'
import { useTranslation } from 'next-i18next'
import * as Dialog from '@radix-ui/react-dialog'
import CrossIcon from "~public/icons/Cross.svg";
import "./index.scss";
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>
<span>{t('modals.about.title')}</span>
</li>
</Dialog.Trigger>
<Dialog.Portal>
@ -22,7 +22,7 @@ const AboutModal = () => {
>
<div className="DialogHeader">
<Dialog.Title className="DialogTitle">
{t("menu.about")}
{t('menu.about')}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild>
<span>
@ -33,7 +33,7 @@ const AboutModal = () => {
<section>
<Dialog.Description className="DialogDescription">
Granblue.team is a tool to save and share team compositions for{" "}
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">
@ -49,10 +49,10 @@ const AboutModal = () => {
<section>
<Dialog.Title className="DialogTitle">Credits</Dialog.Title>
<Dialog.Description className="DialogDescription">
Granblue.team was built by{" "}
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{" "}
help from{' '}
<a href="https://twitter.com/lalalalinna">@lalalalinna</a> and{' '}
<a href="https://twitter.com/tarngerine">@tarngerine</a>.
</Dialog.Description>
</section>
@ -67,7 +67,7 @@ const AboutModal = () => {
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default AboutModal;
export default AboutModal

View file

@ -22,7 +22,7 @@
box-shadow: 0 0 0 2px $grey-10;
}
&[data-state="checked"] {
&[data-state='checked'] {
background: $grey-10;
}
}
@ -40,7 +40,7 @@
cursor: pointer;
}
&[data-state="checked"] {
&[data-state='checked'] {
background: $grey-100;
transform: translateX(21px);
}
@ -75,7 +75,7 @@
gap: $unit * 2;
select {
background: no-repeat url("/icons/ArrowDark.svg"), $grey-90;
background: no-repeat url('/icons/ArrowDark.svg'), $grey-90;
background-position-y: center;
background-position-x: 95%;
margin: 0;

View file

@ -1,35 +1,33 @@
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 { account } = useSnapshot(accountState)
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
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 = {};
const headers = {}
// cookies.account != null
// ? {
// headers: {
@ -39,17 +37,17 @@ const AccountModal = () => {
// : {}
// State
const [open, setOpen] = useState(false);
const [picture, setPicture] = useState("");
const [language, setLanguage] = useState("");
const [gender, setGender] = useState(0);
const [privateProfile, setPrivateProfile] = useState(false);
const [open, setOpen] = useState(false)
const [picture, setPicture] = useState('')
const [language, setLanguage] = useState('')
const [gender, setGender] = useState(0)
const [privateProfile, setPrivateProfile] = useState(false)
// Refs
const pictureSelect = React.createRef<HTMLSelectElement>();
const languageSelect = React.createRef<HTMLSelectElement>();
const genderSelect = React.createRef<HTMLSelectElement>();
const privateSelect = React.createRef<HTMLInputElement>();
const pictureSelect = React.createRef<HTMLSelectElement>()
const languageSelect = React.createRef<HTMLSelectElement>()
const genderSelect = React.createRef<HTMLSelectElement>()
const privateSelect = React.createRef<HTMLInputElement>()
// useEffect(() => {
// if (cookies.user) setPicture(cookies.user.picture)
@ -64,27 +62,27 @@ const AccountModal = () => {
<option key={`picture-${i}`} value={item.filename}>
{item.name[locale]}
</option>
);
});
)
})
function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (pictureSelect.current) setPicture(pictureSelect.current.value);
if (pictureSelect.current) setPicture(pictureSelect.current.value)
}
function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (languageSelect.current) setLanguage(languageSelect.current.value);
if (languageSelect.current) setLanguage(languageSelect.current.value)
}
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (genderSelect.current) setGender(parseInt(genderSelect.current.value));
if (genderSelect.current) setGender(parseInt(genderSelect.current.value))
}
function handlePrivateChange(checked: boolean) {
setPrivateProfile(checked);
setPrivateProfile(checked)
}
function update(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
event.preventDefault()
const object = {
user: {
@ -94,7 +92,7 @@ const AccountModal = () => {
gender: gender,
private: privateProfile,
},
};
}
// api.endpoints.users
// .update(cookies.account.user_id, object, headers)
@ -131,14 +129,14 @@ const AccountModal = () => {
}
function openChange(open: boolean) {
setOpen(open);
setOpen(open)
}
return (
<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>
@ -149,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}
@ -165,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
@ -192,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
@ -202,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
@ -221,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>
@ -245,13 +243,13 @@ const AccountModal = () => {
</Switch.Root>
</div>
<Button>{t("modals.settings.buttons.confirm")}</Button>
<Button>{t('modals.settings.buttons.confirm')}</Button>
</form>
</Dialog.Content>
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default AccountModal;
export default AccountModal

View file

@ -1,19 +1,19 @@
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 {
open: boolean;
title?: string;
message: string;
primaryAction?: () => void;
primaryActionText?: string;
cancelAction: () => void;
cancelActionText: string;
open: boolean
title?: string
message: string
primaryAction?: () => void
primaryActionText?: string
cancelAction: () => void
cancelActionText: string
}
const Alert = (props: 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,14 +38,14 @@ const Alert = (props: Props) => {
{props.primaryActionText}
</AlertDialog.Action>
) : (
""
''
)}
</div>
</AlertDialog.Content>
</div>
</AlertDialog.Portal>
</AlertDialog.Root>
);
};
)
}
export default Alert;
export default Alert

View file

@ -1,82 +1,80 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from "classnames";
import classNames from 'classnames'
import { axData } from "~utils/axData";
import { axData } from '~utils/axData'
import "./index.scss";
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;
axType: number
currentSkills?: SimpleAxSkill[]
sendValidity: (isValid: boolean) => void
sendValues: (
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) => void;
) => void
}
const AXSelect = (props: Props) => {
const router = useRouter();
const router = useRouter()
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const { t } = useTranslation("common");
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({
axValue1: "",
axValue2: "",
});
axValue1: '',
axValue2: '',
})
const primaryErrorClasses = classNames({
errors: true,
visible: errors.axValue1.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>();
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);
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);
setPrimaryAxModifier(props.currentSkills[0].modifier)
setPrimaryAxValue(props.currentSkills[0].strength);
setPrimaryAxValue(props.currentSkills[0].strength)
}
if (props.currentSkills && props.currentSkills[1]) {
if (props.currentSkills[1].modifier != null)
setSecondaryAxModifier(props.currentSkills[1].modifier);
setSecondaryAxModifier(props.currentSkills[1].modifier)
setSecondaryAxValue(props.currentSkills[1].strength);
setSecondaryAxValue(props.currentSkills[1].strength)
}
}, [props.currentSkills]);
}, [props.currentSkills])
useEffect(() => {
props.sendValues(
@ -84,201 +82,198 @@ const AXSelect = (props: Props) => {
primaryAxValue,
secondaryAxModifier,
secondaryAxValue
);
)
}, [
props,
primaryAxModifier,
primaryAxValue,
secondaryAxModifier,
secondaryAxValue,
]);
])
useEffect(() => {
props.sendValidity(
primaryAxValue > 0 && errors.axValue1 === "" && errors.axValue2 === ""
);
}, [props, primaryAxValue, errors]);
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];
const axOptions = axData[props.axType - 1]
let axOptionElements: React.ReactNode[] = [];
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;
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];
const primarySkill = axOptions[modifier]
if (primarySkill.secondary) {
const secondaryAxOptions = 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")}
{t('ax.no_skill')}
</option>
);
return axOptionElements;
)
return axOptionElements
}
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const value = parseInt(event.target.value);
const value = parseInt(event.target.value)
if (primaryAxModifierSelect.current == event.target) {
setPrimaryAxModifier(value);
setPrimaryAxModifier(value)
if (
primaryAxValueInput.current &&
secondaryAxModifierSelect.current &&
secondaryAxValueInput.current
) {
setupInput(
axData[props.axType - 1][value],
primaryAxValueInput.current
);
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
secondaryAxModifierSelect.current.value = "-1";
secondaryAxValueInput.current.value = "";
secondaryAxModifierSelect.current.value = '-1'
secondaryAxValueInput.current.value = ''
}
} else {
setSecondaryAxModifier(value);
setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier];
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
const currentAxSkill = primaryAxSkill.secondary
? primaryAxSkill.secondary.find((skill) => skill.id == value)
: undefined;
: undefined
if (secondaryAxValueInput.current)
setupInput(currentAxSkill, secondaryAxValueInput.current);
setupInput(currentAxSkill, secondaryAxValueInput.current)
}
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value);
let newErrors = { ...errors };
const value = parseFloat(event.target.value)
let newErrors = { ...errors }
if (primaryAxValueInput.current == event.target) {
if (handlePrimaryErrors(value)) setPrimaryAxValue(value);
if (handlePrimaryErrors(value)) setPrimaryAxValue(value)
} else {
if (handleSecondaryErrors(value)) setSecondaryAxValue(value);
if (handleSecondaryErrors(value)) setSecondaryAxValue(value)
}
}
function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier];
let newErrors = { ...errors };
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = { ...errors }
if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = t("ax.errors.value_too_low", {
newErrors.axValue1 = t('ax.errors.value_too_low', {
name: primaryAxSkill.name[locale],
minValue: primaryAxSkill.minValue,
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : "",
});
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
})
} else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = t("ax.errors.value_too_high", {
newErrors.axValue1 = t('ax.errors.value_too_high', {
name: primaryAxSkill.name[locale],
maxValue: primaryAxSkill.minValue,
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : "",
});
suffix: primaryAxSkill.suffix ? primaryAxSkill.suffix : '',
})
} else if (!value || value <= 0) {
newErrors.axValue1 = t("ax.errors.value_empty", {
newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill.name[locale],
});
})
} else {
newErrors.axValue1 = "";
newErrors.axValue1 = ''
}
setErrors(newErrors);
setErrors(newErrors)
return newErrors.axValue1.length === 0;
return newErrors.axValue1.length === 0
}
function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier];
let newErrors = { ...errors };
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", {
newErrors.axValue2 = t('ax.errors.value_too_low', {
name: secondaryAxSkill.name[locale],
minValue: secondaryAxSkill.minValue,
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : "",
});
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
})
} else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = t("ax.errors.value_too_high", {
newErrors.axValue2 = t('ax.errors.value_too_high', {
name: secondaryAxSkill.name[locale],
maxValue: secondaryAxSkill.minValue,
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : "",
});
suffix: secondaryAxSkill.suffix ? secondaryAxSkill.suffix : '',
})
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = t("ax.errors.value_not_whole", {
newErrors.axValue2 = t('ax.errors.value_not_whole', {
name: secondaryAxSkill.name[locale],
});
})
} else if (primaryAxValue <= 0) {
newErrors.axValue1 = t("ax.errors.value_empty", {
newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill.name[locale],
});
})
} else {
newErrors.axValue2 = "";
newErrors.axValue2 = ''
}
}
}
setErrors(newErrors);
setErrors(newErrors)
return newErrors.axValue2.length === 0;
return newErrors.axValue2.length === 0
}
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
if (ax) {
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ""}`;
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";
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 = "";
primaryAxValueInput.current.disabled = true
primaryAxValueInput.current.placeholder = ''
}
secondaryAxValueInput.current.disabled = true;
secondaryAxValueInput.current.placeholder = "";
secondaryAxValueInput.current.disabled = true
secondaryAxValueInput.current.placeholder = ''
}
}
}
@ -345,7 +340,7 @@ const AXSelect = (props: Props) => {
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div>
</div>
);
};
)
}
export default AXSelect;
export default AXSelect

View file

@ -1,143 +1,143 @@
import React, { useEffect, useState } from "react";
import classNames from "classnames";
import React, { useEffect, useState } from 'react'
import classNames from 'classnames'
import Link from "next/link";
import Link from 'next/link'
import AddIcon from "~public/icons/Add.svg";
import CheckIcon from "~public/icons/LargeCheck.svg";
import CrossIcon from "~public/icons/Cross.svg";
import EditIcon from "~public/icons/Edit.svg";
import LinkIcon from "~public/icons/Link.svg";
import MenuIcon from "~public/icons/Menu.svg";
import SaveIcon from "~public/icons/Save.svg";
import SettingsIcon from "~public/icons/Settings.svg";
import AddIcon from '~public/icons/Add.svg'
import CheckIcon from '~public/icons/LargeCheck.svg'
import CrossIcon from '~public/icons/Cross.svg'
import EditIcon from '~public/icons/Edit.svg'
import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg'
import SaveIcon from '~public/icons/Save.svg'
import SettingsIcon from '~public/icons/Settings.svg'
import "./index.scss";
import './index.scss'
import { ButtonType } from "~utils/enums";
import { ButtonType } from '~utils/enums'
interface Props {
active?: boolean;
disabled?: boolean;
classes?: string[];
icon?: string;
type?: ButtonType;
children?: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
active?: boolean
disabled?: boolean
classes?: string[]
icon?: string
type?: ButtonType
children?: React.ReactNode
onClick?: (event: React.MouseEvent<HTMLElement>) => void
}
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 [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",
'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]);
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;
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;
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;
return icon
}
function handleMouseDown() {
setPressed(true);
setPressed(true)
}
function handleMouseUp() {
setPressed(false);
setPressed(false)
}
return (
<button
@ -152,10 +152,10 @@ const Button = (props: Props) => {
{props.type != ButtonType.IconOnly ? (
<span className="text">{props.children}</span>
) : (
""
''
)}
</button>
);
};
)
}
export default Button;
export default Button

View file

@ -1,33 +1,33 @@
import React, { useEffect, useState } from "react";
import "./index.scss";
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 fieldType = ['password', 'confirm_password'].includes(props.fieldName)
? 'password'
: 'text'
const [currentCount, setCurrentCount] = useState(0);
const [currentCount, setCurrentCount] = useState(0)
useEffect(() => {
setCurrentCount(
props.value ? props.limit - props.value.length : props.limit
);
}, [props.limit, props.value]);
)
}, [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 (
@ -39,7 +39,7 @@ const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
type={fieldType}
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ""}
defaultValue={props.value || ''}
onBlur={props.onBlur}
onChange={onChange}
maxLength={props.limit}
@ -50,8 +50,8 @@ const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
</div>
{props.error.length > 0 && <p className="InputError">{props.error}</p>}
</fieldset>
);
)
}
);
)
export default CharLimitedFieldset;
export default CharLimitedFieldset

View file

@ -1,70 +1,70 @@
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;
incomingCharacter?: Character;
conflictingCharacters?: GridCharacter[];
desiredPosition: number;
resolveConflict: () => void;
resetConflict: () => void;
open: boolean
incomingCharacter?: Character
conflictingCharacters?: GridCharacter[]
desiredPosition: number
resolveConflict: () => void
resetConflict: () => void
}
const CharacterConflictModal = (props: Props) => {
const { t } = useTranslation("common");
const { t } = useTranslation('common')
// States
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(false)
useEffect(() => {
setOpen(props.open);
}, [setOpen, props.open]);
setOpen(props.open)
}, [setOpen, props.open])
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") {
let element = 1;
if (character?.granblue_id === '3030182000') {
let element = 1
if (
appState.grid.weapons.mainWeapon &&
appState.grid.weapons.mainWeapon.element
) {
element = appState.grid.weapons.mainWeapon.element;
element = appState.grid.weapons.mainWeapon.element
} else if (appState.party.element != 0) {
element = appState.party.element;
element = appState.party.element
}
suffix = `${suffix}_0${element}`;
suffix = `${suffix}_0${element}`
}
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${character?.granblue_id}_${suffix}.jpg`;
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${character?.granblue_id}_${suffix}.jpg`
}
function openChange(open: boolean) {
setOpen(open);
setOpen(open)
}
function close() {
setOpen(false);
props.resetConflict();
setOpen(false)
props.resetConflict()
}
return (
@ -107,7 +107,7 @@ const CharacterConflictModal = (props: Props) => {
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default CharacterConflictModal;
export default CharacterConflictModal

View file

@ -1,68 +1,68 @@
/* 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 {
new: boolean;
characters?: GridCharacter[];
createParty: () => Promise<AxiosResponse<any, any>>;
pushHistory?: (path: string) => void;
new: boolean
characters?: GridCharacter[]
createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void
}
const CharacterGrid = (props: Props) => {
// Constants
const numCharacters: number = 5;
const numCharacters: number = 5
// Cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {};
: {}
// Set up state for view management
const { party, grid } = useSnapshot(appState);
const [slug, setSlug] = useState();
const [modalOpen, setModalOpen] = useState(false);
const { party, grid } = useSnapshot(appState)
const [slug, setSlug] = useState()
const [modalOpen, setModalOpen] = useState(false)
// Set up state for conflict management
const [incoming, setIncoming] = useState<Character>();
const [conflicts, setConflicts] = useState<GridCharacter[]>([]);
const [position, setPosition] = useState(0);
const [incoming, setIncoming] = useState<Character>()
const [conflicts, setConflicts] = useState<GridCharacter[]>([])
const [position, setPosition] = useState(0)
// Set up state for data
const [job, setJob] = useState<Job | undefined>();
const [job, setJob] = useState<Job | undefined>()
const [jobSkills, setJobSkills] = useState<JobSkillObject>({
0: undefined,
1: undefined,
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<{
[key: number]: number | undefined;
}>({});
[key: number]: number | undefined
}>({})
// Set the editable flag only on first load
useEffect(() => {
@ -71,58 +71,58 @@ const CharacterGrid = (props: Props) => {
(accountData && party.user && accountData.userId === party.user.id) ||
props.new
)
appState.party.editable = true;
else appState.party.editable = false;
}, [props.new, accountData, party]);
appState.party.editable = true
else appState.party.editable = false
}, [props.new, accountData, party])
useEffect(() => {
setJob(appState.party.job);
setJobSkills(appState.party.jobSkills);
}, [appState]);
setJob(appState.party.job)
setJobSkills(appState.party.jobSkills)
}, [appState])
// Initialize an array of current uncap values for each characters
useEffect(() => {
let initialPreviousUncapValues: { [key: number]: number } = {};
let initialPreviousUncapValues: { [key: number]: number } = {}
Object.values(appState.grid.characters).map((o) => {
o ? (initialPreviousUncapValues[o.position] = o.uncap_level) : 0;
});
setPreviousUncapValues(initialPreviousUncapValues);
}, [appState.grid.characters]);
o ? (initialPreviousUncapValues[o.position] = o.uncap_level) : 0
})
setPreviousUncapValues(initialPreviousUncapValues)
}, [appState.grid.characters])
// Methods: Adding an object from search
function receiveCharacterFromSearch(
object: SearchableObject,
position: number
) {
const character = object as Character;
const character = object as Character
if (!party.id) {
props.createParty().then((response) => {
const party = response.data.party;
appState.party.id = party.id;
setSlug(party.shortcode);
const party = response.data.party
appState.party.id = party.id
setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`);
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveCharacter(party.id, character, position)
.then((response) => storeGridCharacter(response.data.grid_character))
.catch((error) => console.error(error));
});
.catch((error) => console.error(error))
})
} else {
if (party.editable)
saveCharacter(party.id, character, position)
.then((response) => handleCharacterResponse(response.data))
.catch((error) => console.error(error));
.catch((error) => console.error(error))
}
}
async function handleCharacterResponse(data: any) {
if (data.hasOwnProperty("conflicts")) {
setIncoming(data.incoming);
setConflicts(data.conflicts);
setPosition(data.position);
setModalOpen(true);
if (data.hasOwnProperty('conflicts')) {
setIncoming(data.incoming)
setConflicts(data.conflicts)
setPosition(data.position)
setModalOpen(true)
} else {
storeGridCharacter(data.grid_character);
storeGridCharacter(data.grid_character)
}
}
@ -141,11 +141,11 @@ const CharacterGrid = (props: Props) => {
},
},
headers
);
)
}
function storeGridCharacter(gridCharacter: GridCharacter) {
appState.grid.characters[gridCharacter.position] = gridCharacter;
appState.grid.characters[gridCharacter.position] = gridCharacter
}
async function resolveConflict() {
@ -159,128 +159,128 @@ const CharacterGrid = (props: Props) => {
})
.then((response) => {
// Store new character in state
storeGridCharacter(response.data.grid_character);
storeGridCharacter(response.data.grid_character)
// Remove conflicting characters from state
conflicts.forEach(
(c) => (appState.grid.characters[c.position] = undefined)
);
)
// Reset conflict
resetConflict();
resetConflict()
// Close modal
setModalOpen(false);
});
setModalOpen(false)
})
}
}
function resetConflict() {
setPosition(-1);
setConflicts([]);
setIncoming(undefined);
setPosition(-1)
setConflicts([])
setIncoming(undefined)
}
// Methods: Saving job and job skills
const saveJob = function (job: Job) {
const payload = {
party: {
job_id: job ? job.id : "",
job_id: job ? job.id : '',
},
...headers,
};
}
if (party.id && appState.party.editable) {
api.updateJob({ partyId: party.id, params: payload }).then((response) => {
const newParty = response.data.party;
const newParty = response.data.party
setJob(newParty.job);
appState.party.job = newParty.job;
setJob(newParty.job)
appState.party.job = newParty.job
setJobSkills(newParty.job_skills);
appState.party.jobSkills = newParty.job_skills;
});
setJobSkills(newParty.job_skills)
appState.party.jobSkills = newParty.job_skills
})
}
}
};
const saveJobSkill = function (skill: JobSkill, position: number) {
if (party.id && appState.party.editable) {
const positionedKey = `skill${position}_id`;
const positionedKey = `skill${position}_id`
let skillObject: {
[key: string]: string | undefined;
skill0_id?: string;
skill1_id?: string;
skill2_id?: string;
skill3_id?: string;
} = {};
[key: string]: string | undefined
skill0_id?: string
skill1_id?: string
skill2_id?: string
skill3_id?: string
} = {}
const payload = {
party: skillObject,
...headers,
};
}
skillObject[positionedKey] = skill.id;
skillObject[positionedKey] = skill.id
api
.updateJobSkills({ partyId: party.id, params: payload })
.then((response) => {
// Update the current skills
const newSkills = response.data.party.job_skills;
setJobSkills(newSkills);
appState.party.jobSkills = newSkills;
const newSkills = response.data.party.job_skills
setJobSkills(newSkills)
appState.party.jobSkills = newSkills
})
.catch((error) => {
const data = error.response.data;
if (data.code == "too_many_skills_of_type") {
const data = error.response.data
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.`;
setErrorMessage(message);
} skills to your party at once.`
setErrorMessage(message)
}
console.log(error.response.data)
})
}
console.log(error.response.data);
});
}
};
// Methods: Helpers
function characterUncapLevel(character: Character) {
let uncapLevel;
let uncapLevel
if (character.special) {
uncapLevel = 3;
if (character.uncap.ulb) uncapLevel = 5;
else if (character.uncap.flb) uncapLevel = 4;
uncapLevel = 3
if (character.uncap.ulb) uncapLevel = 5
else if (character.uncap.flb) uncapLevel = 4
} else {
uncapLevel = 4;
if (character.uncap.ulb) uncapLevel = 6;
else if (character.uncap.flb) uncapLevel = 5;
uncapLevel = 4
if (character.uncap.ulb) uncapLevel = 6
else if (character.uncap.flb) uncapLevel = 5
}
return uncapLevel;
return uncapLevel
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
storePreviousUncapValue(position);
storePreviousUncapValue(position)
try {
if (uncapLevel != previousUncapValues[position])
await api.updateUncap("character", id, uncapLevel).then((response) => {
storeGridCharacter(response.data.grid_character);
});
await api.updateUncap('character', id, uncapLevel).then((response) => {
storeGridCharacter(response.data.grid_character)
})
} catch (error) {
console.error(error);
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position]);
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = { ...previousUncapValues };
delete newPreviousValues[position];
setPreviousUncapValues(newPreviousValues);
let newPreviousValues = { ...previousUncapValues }
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
@ -289,50 +289,50 @@ const CharacterGrid = (props: Props) => {
position: number,
uncapLevel: number
) {
memoizeAction(id, position, uncapLevel);
memoizeAction(id, position, uncapLevel)
// Optimistically update UI
updateUncapLevel(position, uncapLevel);
updateUncapLevel(position, uncapLevel)
}
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel);
debouncedAction(id, position, uncapLevel)
},
[props, previousUncapValues]
);
)
const debouncedAction = useMemo(
() =>
debounce((id, position, number) => {
saveUncap(id, position, number);
saveUncap(id, position, number)
}, 500),
[props, saveUncap]
);
)
const updateUncapLevel = (
position: number,
uncapLevel: number | undefined
) => {
const character = appState.grid.characters[position];
const character = appState.grid.characters[position]
if (character && uncapLevel) {
character.uncap_level = uncapLevel;
appState.grid.characters[position] = character;
character.uncap_level = uncapLevel
appState.grid.characters[position] = character
}
}
};
function storePreviousUncapValue(position: number) {
// Save the current value in case of an unexpected result
let newPreviousValues = { ...previousUncapValues };
let newPreviousValues = { ...previousUncapValues }
if (grid.characters[position]) {
newPreviousValues[position] = grid.characters[position]?.uncap_level;
setPreviousUncapValues(newPreviousValues);
newPreviousValues[position] = grid.characters[position]?.uncap_level
setPreviousUncapValues(newPreviousValues)
}
}
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
@ -372,12 +372,12 @@ const CharacterGrid = (props: Props) => {
updateUncap={initiateUncapUpdate}
/>
</li>
);
)
})}
</ul>
</div>
</div>
);
};
)
}
export default CharacterGrid;
export default CharacterGrid

View file

@ -1,71 +1,69 @@
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 * as HoverCard from "@radix-ui/react-hover-card";
import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import "./index.scss";
import './index.scss'
interface Props {
gridCharacter: GridCharacter;
children: React.ReactNode;
gridCharacter: GridCharacter
children: React.ReactNode
}
interface KeyNames {
[key: string]: {
en: string;
jp: string;
};
en: string
jp: string
}
}
const CharacterHovercard = (props: Props) => {
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
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 Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = [
"none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const tintElement = Element[props.gridCharacter.object.element];
const tintElement = Element[props.gridCharacter.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(
" ",
"_"
)}`;
' ',
'_'
)}`
function characterImage() {
let imgSrc = "";
let imgSrc = ''
if (props.gridCharacter) {
const character = props.gridCharacter.object;
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'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
}
return imgSrc;
return imgSrc
}
return (
@ -101,7 +99,7 @@ const CharacterHovercard = (props: Props) => {
}
/>
) : (
""
''
)}
</div>
<UncapIndicator
@ -114,12 +112,12 @@ const CharacterHovercard = (props: Props) => {
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t("buttons.wiki")}
{t('buttons.wiki')}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
);
};
)
}
export default CharacterHovercard;
export default CharacterHovercard

View file

@ -1,36 +1,34 @@
import React from "react";
import { useRouter } from "next/router";
import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import "./index.scss";
import './index.scss'
interface Props {
data: Character;
onClick: () => void;
data: Character
onClick: () => void
}
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const CharacterResult = (props: Props) => {
const router = useRouter();
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 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`;
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`;
if (character.granblue_id === '3030182000') {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`
}
return url;
};
return url
}
return (
<li className="CharacterResult" onClick={props.onClick}>
@ -48,7 +46,7 @@ const CharacterResult = (props: Props) => {
</div>
</div>
</li>
);
};
)
}
export default CharacterResult;
export default CharacterResult

View file

@ -1,134 +1,134 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import cloneDeep from "lodash.clonedeep";
import cloneDeep from 'lodash.clonedeep'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import SearchFilter from "~components/SearchFilter";
import SearchFilterCheckboxItem from "~components/SearchFilterCheckboxItem";
import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import "./index.scss";
import './index.scss'
import {
emptyElementState,
emptyProficiencyState,
emptyRarityState,
} from "~utils/emptyStates";
import { elements, proficiencies, rarities } from "~utils/stateValues";
} 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 [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] =
useState<ElementState>(emptyElementState);
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);
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);
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);
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);
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);
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);
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);
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);
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);
.map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const checkedProficiency1Filters = Object.values(proficiency1State)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const checkedProficiency2Filters = Object.values(proficiency2State)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiency1Filters,
proficiency2: checkedProficiency2Filters,
};
}
props.sendFilters(filters);
props.sendFilters(filters)
}
useEffect(() => {
sendFilters();
}, [rarityState, elementState, proficiency1State, proficiency2State]);
sendFilters()
}, [rarityState, elementState, proficiency1State, proficiency2State])
function renderProficiencyFilter(proficiency: 1 | 2) {
const onCheckedChange =
proficiency == 1 ? handleProficiency1Change : handleProficiency2Change;
proficiency == 1 ? handleProficiency1Change : handleProficiency2Change
const numSelected =
proficiency == 1
? Object.values(proficiency1State)
@ -136,20 +136,20 @@ const CharacterSearchFilterBar = (props: Props) => {
.filter(Boolean).length
: Object.values(proficiency2State)
.map((x) => x.checked)
.filter(Boolean).length;
const open = proficiency == 1 ? proficiency1Menu : proficiency2Menu;
.filter(Boolean).length
const open = proficiency == 1 ? proficiency1Menu : proficiency2Menu
const onOpenChange =
proficiency == 1 ? proficiency1MenuOpened : proficiency2MenuOpened;
proficiency == 1 ? proficiency1MenuOpened : proficiency2MenuOpened
return (
<SearchFilter
label={`${t("filters.labels.proficiency")} ${proficiency}`}
label={`${t('filters.labels.proficiency')} ${proficiency}`}
numSelected={numSelected}
open={open}
onOpenChange={onOpenChange}
>
<DropdownMenu.Label className="Label">{`${t(
"filters.labels.proficiency"
'filters.labels.proficiency'
)} ${proficiency}`}</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
@ -157,7 +157,7 @@ const CharacterSearchFilterBar = (props: Props) => {
const checked =
proficiency == 1
? proficiency1State[proficiencies[i]].checked
: proficiency2State[proficiencies[i]].checked;
: proficiency2State[proficiencies[i]].checked
return (
<SearchFilterCheckboxItem
@ -168,7 +168,7 @@ const CharacterSearchFilterBar = (props: Props) => {
>
{t(`proficiencies.${proficiencies[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
@ -180,7 +180,7 @@ const CharacterSearchFilterBar = (props: Props) => {
].checked
: proficiency2State[
proficiencies[i + proficiencies.length / 2]
].checked;
].checked
return (
<SearchFilterCheckboxItem
@ -195,18 +195,18 @@ const CharacterSearchFilterBar = (props: Props) => {
}`
)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
</section>
</SearchFilter>
);
)
}
return (
<div className="SearchFilterBar">
<SearchFilter
label={t("filters.labels.rarity")}
label={t('filters.labels.rarity')}
numSelected={
Object.values(rarityState)
.map((x) => x.checked)
@ -216,7 +216,7 @@ const CharacterSearchFilterBar = (props: Props) => {
onOpenChange={rarityMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.rarity")}
{t('filters.labels.rarity')}
</DropdownMenu.Label>
{Array.from(Array(rarities.length)).map((x, i) => {
return (
@ -228,12 +228,12 @@ const CharacterSearchFilterBar = (props: Props) => {
>
{t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</SearchFilter>
<SearchFilter
label={t("filters.labels.element")}
label={t('filters.labels.element')}
numSelected={
Object.values(elementState)
.map((x) => x.checked)
@ -243,7 +243,7 @@ const CharacterSearchFilterBar = (props: Props) => {
onOpenChange={elementMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.element")}
{t('filters.labels.element')}
</DropdownMenu.Label>
{Array.from(Array(elements.length)).map((x, i) => {
return (
@ -255,14 +255,14 @@ const CharacterSearchFilterBar = (props: Props) => {
>
{t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</SearchFilter>
{renderProficiencyFilter(1)}
{renderProficiencyFilter(2)}
</div>
);
};
)
}
export default CharacterSearchFilterBar;
export default CharacterSearchFilterBar

View file

@ -1,87 +1,85 @@
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;
position: number;
editable: boolean;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
gridCharacter?: GridCharacter
position: number
editable: boolean
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const CharacterUnit = (props: Props) => {
const { t } = useTranslation("common");
const { t } = useTranslation('common')
const { party, grid } = useSnapshot(appState);
const { party, grid } = useSnapshot(appState)
const router = useRouter();
const router = useRouter()
const locale =
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,
editable: props.editable,
filled: props.gridCharacter !== undefined,
});
})
const gridCharacter = props.gridCharacter;
const character = gridCharacter?.object;
const gridCharacter = props.gridCharacter
const character = gridCharacter?.object
useEffect(() => {
generateImageUrl();
});
generateImageUrl()
})
function generateImageUrl() {
let imgSrc = "";
let imgSrc = ''
if (props.gridCharacter) {
const character = props.gridCharacter.object!;
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") {
let element = 1;
if (props.gridCharacter.object.granblue_id === '3030182000') {
let element = 1
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
element = grid.weapons.mainWeapon.element;
element = grid.weapons.mainWeapon.element
} else if (party.element != 0) {
element = party.element;
element = party.element
}
suffix = `${suffix}_0${element}`;
suffix = `${suffix}_0${element}`
}
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${suffix}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_${suffix}.jpg`
}
setImageUrl(imgSrc);
setImageUrl(imgSrc)
}
function passUncapData(uncap: number) {
if (props.gridCharacter)
props.updateUncap(props.gridCharacter.id, props.position, uncap);
props.updateUncap(props.gridCharacter.id, props.position, uncap)
}
const image = (
@ -92,21 +90,21 @@ 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}
>
{image}
</SearchModal>
);
)
const unitContent = (
<div className={classes}>
@ -121,19 +119,19 @@ const CharacterUnit = (props: Props) => {
special={character.special}
/>
) : (
""
''
)}
<h3 className="CharacterName">{character?.name[locale]}</h3>
</div>
);
)
const withHovercard = (
<CharacterHovercard gridCharacter={gridCharacter!}>
{unitContent}
</CharacterHovercard>
);
)
return gridCharacter && !props.editable ? withHovercard : unitContent;
};
return gridCharacter && !props.editable ? withHovercard : unitContent
}
export default CharacterUnit;
export default CharacterUnit

View file

@ -27,7 +27,7 @@
}
&:hover,
&[data-state="on"] {
&[data-state='on'] {
background: $grey-80;
color: $grey-10;

View file

@ -1,23 +1,21 @@
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 * as ToggleGroup from "@radix-ui/react-toggle-group";
import * as ToggleGroup from '@radix-ui/react-toggle-group'
import "./index.scss";
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 router = useRouter()
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'
return (
<ToggleGroup.Root
@ -32,52 +30,52 @@ const ElementToggle = (props: Props) => {
value="0"
aria-label="null"
>
{t("elements.null")}
{t('elements.null')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem wind ${locale}`}
value="1"
aria-label="wind"
>
{t("elements.wind")}
{t('elements.wind')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem fire ${locale}`}
value="2"
aria-label="fire"
>
{t("elements.fire")}
{t('elements.fire')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem water ${locale}`}
value="3"
aria-label="water"
>
{t("elements.water")}
{t('elements.water')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem earth ${locale}`}
value="4"
aria-label="earth"
>
{t("elements.earth")}
{t('elements.earth')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem dark ${locale}`}
value="5"
aria-label="dark"
>
{t("elements.dark")}
{t('elements.dark')}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem light ${locale}`}
value="6"
aria-label="light"
>
{t("elements.light")}
{t('elements.light')}
</ToggleGroup.Item>
</ToggleGroup.Root>
);
};
)
}
export default ElementToggle;
export default ElementToggle

View file

@ -1,28 +1,28 @@
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 {
grid: GridArray<GridSummon>;
editable: boolean;
exists: boolean;
found?: boolean;
offset: number;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
grid: GridArray<GridSummon>
editable: boolean
exists: boolean
found?: boolean
offset: number
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const ExtraSummons = (props: Props) => {
const numSummons: number = 2;
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 (
@ -36,11 +36,11 @@ const ExtraSummons = (props: Props) => {
updateUncap={props.updateUncap}
/>
</li>
);
)
})}
</ul>
</div>
);
};
)
}
export default ExtraSummons;
export default ExtraSummons

View file

@ -1,28 +1,28 @@
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 {
grid: GridArray<GridWeapon>;
editable: boolean;
found?: boolean;
offset: number;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
grid: GridArray<GridWeapon>
editable: boolean
found?: boolean
offset: number
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const ExtraWeapons = (props: Props) => {
const numWeapons: number = 3;
const { t } = useTranslation("common");
const numWeapons: number = 3
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 (
@ -36,11 +36,11 @@ const ExtraWeapons = (props: Props) => {
updateUncap={props.updateUncap}
/>
</li>
);
)
})}
</ul>
</div>
);
};
)
}
export default ExtraWeapons;
export default ExtraWeapons

View file

@ -1,22 +1,22 @@
import React from "react";
import "./index.scss";
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;
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";
const fieldType = ['password', 'confirm_password'].includes(props.fieldName)
? 'password'
: 'text'
return (
<fieldset className="Fieldset">
@ -26,7 +26,7 @@ const Fieldset = React.forwardRef<HTMLInputElement, Props>(function fieldSet(
type={fieldType}
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ""}
defaultValue={props.value || ''}
onBlur={props.onBlur}
onChange={props.onChange}
ref={ref}
@ -34,7 +34,7 @@ const Fieldset = React.forwardRef<HTMLInputElement, Props>(function fieldSet(
/>
{props.error.length > 0 && <p className="InputError">{props.error}</p>}
</fieldset>
);
});
)
})
export default Fieldset;
export default Fieldset

View file

@ -1,59 +1,59 @@
import React from "react";
import { useTranslation } from "next-i18next";
import classNames from "classnames";
import React from 'react'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import RaidDropdown from "~components/RaidDropdown";
import RaidDropdown from '~components/RaidDropdown'
import "./index.scss";
import './index.scss'
interface Props {
children: React.ReactNode;
scrolled: boolean;
element?: number;
raidSlug?: string;
recency?: number;
children: React.ReactNode
scrolled: boolean
element?: number
raidSlug?: string
recency?: number
onFilter: ({
element,
raidSlug,
recency,
}: {
element?: number;
raidSlug?: string;
recency?: number;
}) => void;
element?: number
raidSlug?: string
recency?: number
}) => void
}
const FilterBar = (props: Props) => {
// Set up translation
const { t } = useTranslation("common");
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 elementSelect = React.createRef<HTMLSelectElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const recencySelect = React.createRef<HTMLSelectElement>()
// Set up classes object for showing shadow on scroll
const classes = classNames({
FilterBar: true,
shadow: props.scrolled,
});
})
function elementSelectChanged() {
const elementValue = elementSelect.current
? parseInt(elementSelect.current.value)
: -1;
props.onFilter({ element: elementValue });
: -1
props.onFilter({ element: elementValue })
}
function recencySelectChanged() {
const recencyValue = recencySelect.current
? parseInt(recencySelect.current.value)
: -1;
props.onFilter({ recency: recencyValue });
: -1
props.onFilter({ recency: recencyValue })
}
function raidSelectChanged(slug?: string) {
props.onFilter({ raidSlug: slug });
props.onFilter({ raidSlug: slug })
}
return (
@ -65,28 +65,28 @@ const FilterBar = (props: Props) => {
value={props.element}
>
<option data-element="all" key={-1} value={-1}>
{t("elements.full.all")}
{t('elements.full.all')}
</option>
<option data-element="null" key={0} value={0}>
{t("elements.full.null")}
{t('elements.full.null')}
</option>
<option data-element="wind" key={1} value={1}>
{t("elements.full.wind")}
{t('elements.full.wind')}
</option>
<option data-element="fire" key={2} value={2}>
{t("elements.full.fire")}
{t('elements.full.fire')}
</option>
<option data-element="water" key={3} value={3}>
{t("elements.full.water")}
{t('elements.full.water')}
</option>
<option data-element="earth" key={4} value={4}>
{t("elements.full.earth")}
{t('elements.full.earth')}
</option>
<option data-element="dark" key={5} value={5}>
{t("elements.full.dark")}
{t('elements.full.dark')}
</option>
<option data-element="light" key={6} value={6}>
{t("elements.full.light")}
{t('elements.full.light')}
</option>
</select>
<RaidDropdown
@ -97,29 +97,29 @@ const FilterBar = (props: Props) => {
/>
<select onChange={recencySelectChanged} ref={recencySelect}>
<option key={-1} value={-1}>
{t("recency.all_time")}
{t('recency.all_time')}
</option>
<option key={86400} value={86400}>
{t("recency.last_day")}
{t('recency.last_day')}
</option>
<option key={604800} value={604800}>
{t("recency.last_week")}
{t('recency.last_week')}
</option>
<option key={2629746} value={2629746}>
{t("recency.last_month")}
{t('recency.last_month')}
</option>
<option key={7889238} value={7889238}>
{t("recency.last_3_months")}
{t('recency.last_3_months')}
</option>
<option key={15778476} value={15778476}>
{t("recency.last_6_months")}
{t('recency.last_6_months')}
</option>
<option key={31556952} value={31556952}>
{t("recency.last_year")}
{t('recency.last_year')}
</option>
</select>
</div>
);
};
)
}
export default FilterBar;
export default FilterBar

View file

@ -50,8 +50,8 @@
width: 70px;
}
.grid_mainhand img[src*="jpg"],
.grid_weapon img[src*="jpg"] {
.grid_mainhand img[src*='jpg'],
.grid_weapon img[src*='jpg'] {
border-radius: 4px;
width: 100%;
height: 100%;

View file

@ -1,122 +1,120 @@
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 { ButtonType } from '~utils/enums'
import "./index.scss";
import './index.scss'
interface Props {
shortcode: string;
id: string;
name: string;
raid: Raid;
grid: GridWeapon[];
user?: User;
favorited: boolean;
createdAt: Date;
displayUser?: boolean | false;
onClick: (shortcode: string) => void;
onSave?: (partyId: string, favorited: boolean) => void;
shortcode: string
id: string
name: string
raid: Raid
grid: GridWeapon[]
user?: User
favorited: boolean
createdAt: Date
displayUser?: boolean | false
onClick: (shortcode: string) => void
onSave?: (partyId: string, favorited: boolean) => void
}
const GridRep = (props: Props) => {
const numWeapons: number = 9;
const numWeapons: number = 9
const { account } = useSnapshot(accountState);
const { account } = useSnapshot(accountState)
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
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>>({});
const [grid, setGrid] = useState<GridArray<GridWeapon>>({});
const [mainhand, setMainhand] = useState<Weapon>()
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
const [grid, setGrid] = useState<GridArray<GridWeapon>>({})
const titleClass = classNames({
empty: !props.name,
});
})
const raidClass = classNames({
raid: true,
empty: !props.raid,
});
})
const userClass = classNames({
user: true,
empty: !props.user,
});
})
useEffect(() => {
const newWeapons = Array(numWeapons);
const gridWeapons = Array(numWeapons);
const newWeapons = Array(numWeapons)
const gridWeapons = Array(numWeapons)
for (const [key, value] of Object.entries(props.grid)) {
if (value.position == -1) setMainhand(value.object);
if (value.position == -1) setMainhand(value.object)
else if (!value.mainhand && value.position != null) {
newWeapons[value.position] = value.object;
gridWeapons[value.position] = value;
newWeapons[value.position] = value.object
gridWeapons[value.position] = value
}
}
setWeapons(newWeapons);
setGrid(gridWeapons);
}, [props.grid]);
setWeapons(newWeapons)
setGrid(gridWeapons)
}, [props.grid])
function navigate() {
props.onClick(props.shortcode);
props.onClick(props.shortcode)
}
function generateMainhandImage() {
let url = "";
let url = ''
if (mainhand) {
if (mainhand.element == 0 && props.grid[0].element) {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${props.grid[0].element}.jpg`;
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${props.grid[0].element}.jpg`
} else {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`;
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`
}
}
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];
const weapon = weapons[position]
const gridWeapon = grid[position]
if (weapon && gridWeapon) {
if (weapon.element == 0 && gridWeapon.element) {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`;
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`
} else {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`;
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
}
}
return weapons[position] ? (
<img alt={weapons[position]?.name[locale]} src={url} />
) : (
""
);
''
)
}
function sendSaveData() {
if (props.onSave) props.onSave(props.id, props.favorited);
if (props.onSave) props.onSave(props.id, props.favorited)
}
const userImage = () => {
@ -129,35 +127,35 @@ const GridRep = (props: Props) => {
/profile/${props.user.picture.picture}@2x.png 2x`}
src={`/profile/${props.user.picture.picture}.png`}
/>
);
} else return <div className="no-user" />;
};
)
} else return <div className="no-user" />
}
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)}
</time>
</div>
</div>
);
)
const detailsWithUsername = (
<div className="Details">
<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 &&
@ -170,20 +168,20 @@ const GridRep = (props: Props) => {
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)}
</time>
</div>
</div>
);
)
return (
<div className="GridRep">
@ -200,12 +198,12 @@ const GridRep = (props: Props) => {
>
{generateGridImage(i)}
</li>
);
)
})}
</ul>
</div>
</div>
);
};
)
}
export default GridRep;
export default GridRep

View file

@ -1,18 +1,18 @@
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;
children: React.ReactNode
}
const GridRepCollection = (props: Props) => {
const classes = classNames({
GridRepCollection: true,
});
})
return <div className={classes}>{props.children}</div>;
};
return <div className={classes}>{props.children}</div>
}
export default GridRepCollection;
export default GridRepCollection

View file

@ -1,11 +1,11 @@
import React from "react";
import React from 'react'
import "./index.scss";
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) => {
@ -15,7 +15,7 @@ const Header = (props: Props) => {
<div className="push" />
<div id="right">{props.right}</div>
</nav>
);
};
)
}
export default Header;
export default Header

View file

@ -67,7 +67,7 @@
cursor: pointer;
}
&[data-state="checked"] {
&[data-state='checked'] {
background: $grey-100;
transform: translateX(17px);
}

View file

@ -1,51 +1,51 @@
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;
username?: string;
logout?: () => void;
authenticated: boolean
username?: string
logout?: () => void
}
const HeaderMenu = (props: Props) => {
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
const { t } = useTranslation('common')
const accountCookie = getCookie("account");
const accountCookie = getCookie('account')
const accountData: AccountCookie = accountCookie
? JSON.parse(accountCookie as string)
: null;
: null
const userCookie = getCookie("user");
const userCookie = getCookie('user')
const userData: UserCookie = userCookie
? JSON.parse(userCookie as string)
: null;
: null
const localeCookie = getCookie("NEXT_LOCALE");
const localeCookie = getCookie('NEXT_LOCALE')
const [checked, setChecked] = useState(false);
const [checked, setChecked] = useState(false)
useEffect(() => {
const locale = localeCookie;
setChecked(locale === "ja" ? true : false);
}, [localeCookie]);
const locale = localeCookie
setChecked(locale === 'ja' ? true : false)
}, [localeCookie])
function handleCheckedChange(value: boolean) {
const language = value ? "ja" : "en";
setCookie("NEXT_LOCALE", language, { path: "/" });
router.push(router.asPath, undefined, { locale: language });
const language = value ? 'ja' : 'en'
setCookie('NEXT_LOCALE', language, { path: '/' })
router.push(router.asPath, undefined, { locale: language })
}
function authItems() {
@ -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,12 +87,12 @@ 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>
</nav>
);
)
}
function unauthItems() {
@ -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>
@ -132,10 +132,10 @@ const HeaderMenu = (props: Props) => {
<SignupModal />
</div>
</ul>
);
)
}
return props.authenticated ? authItems() : unauthItems();
};
return props.authenticated ? authItems() : unauthItems()
}
export default HeaderMenu;
export default HeaderMenu

View file

@ -1,69 +1,69 @@
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 { appState } from '~utils/appState'
import { jobGroups } from '~utils/jobGroups'
import "./index.scss";
import './index.scss'
// Props
interface Props {
currentJob?: string;
onChange?: (job?: Job) => void;
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
currentJob?: string
onChange?: (job?: Job) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
}
type GroupedJob = { [key: string]: Job[] };
type GroupedJob = { [key: string]: Job[] }
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) {
// Set up router for locale
const router = useRouter();
const locale = router.locale || "en";
const router = useRouter()
const locale = router.locale || 'en'
// Create snapshot of app state
const { party } = useSnapshot(appState);
const { party } = useSnapshot(appState)
// Set up local states for storing jobs
const [currentJob, setCurrentJob] = useState<Job>();
const [jobs, setJobs] = useState<Job[]>();
const [sortedJobs, setSortedJobs] = useState<GroupedJob>();
const [currentJob, setCurrentJob] = useState<Job>()
const [jobs, setJobs] = useState<Job[]>()
const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
// Set current job from state on mount
useEffect(() => {
setCurrentJob(party.job);
}, []);
setCurrentJob(party.job)
}, [])
// Organize jobs into groups on mount
useEffect(() => {
const jobGroups = appState.jobs
.map((job) => job.row)
.filter((value, index, self) => self.indexOf(value) === index);
let groupedJobs: GroupedJob = {};
.filter((value, index, self) => self.indexOf(value) === index)
let groupedJobs: GroupedJob = {}
jobGroups.forEach((group) => {
groupedJobs[group] = appState.jobs.filter((job) => job.row === group);
});
groupedJobs[group] = appState.jobs.filter((job) => job.row === group)
})
setJobs(appState.jobs);
setSortedJobs(groupedJobs);
}, [appState]);
setJobs(appState.jobs)
setSortedJobs(groupedJobs)
}, [appState])
// Set current job on mount
useEffect(() => {
if (jobs && props.currentJob) {
const job = appState.jobs.find((job) => job.id === props.currentJob);
setCurrentJob(job);
const job = appState.jobs.find((job) => job.id === props.currentJob)
setCurrentJob(job)
}
}, [appState, props.currentJob]);
}, [appState, props.currentJob])
// Enable changing select value
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (jobs) {
const job = jobs.find((job) => job.id === event.target.value);
if (props.onChange) props.onChange(job);
setCurrentJob(job);
const job = jobs.find((job) => job.id === event.target.value)
if (props.onChange) props.onChange(job)
setCurrentJob(job)
}
}
@ -79,16 +79,16 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
<option key={i} value={item.id}>
{item.name[locale]}
</option>
);
});
)
})
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale];
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
return (
<optgroup key={group} label={groupName}>
{options}
</optgroup>
);
)
}
return (
@ -104,10 +104,10 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
</option>
{sortedJobs
? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
: ""}
: ''}
</select>
);
)
}
);
)
export default JobDropdown;
export default JobDropdown

View file

@ -31,7 +31,7 @@
$height: 249px;
$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,101 +1,99 @@
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 {
job?: Job;
jobSkills: JobSkillObject;
editable: boolean;
saveJob: (job: Job) => void;
saveSkill: (skill: JobSkill, position: number) => void;
job?: Job
jobSkills: JobSkillObject
editable: boolean
saveJob: (job: Job) => void
saveSkill: (skill: JobSkill, position: number) => void
}
const JobSection = (props: Props) => {
const { party } = useSnapshot(appState);
const { t } = useTranslation("common");
const { party } = useSnapshot(appState)
const { t } = useTranslation('common')
const router = useRouter();
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 [numSkills, setNumSkills] = useState(4);
const [job, setJob] = useState<Job>()
const [imageUrl, setImageUrl] = useState('')
const [numSkills, setNumSkills] = useState(4)
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
[]
);
)
const selectRef = React.createRef<HTMLSelectElement>();
const selectRef = React.createRef<HTMLSelectElement>()
useEffect(() => {
// Set current job based on ID
if (props.job) {
setJob(props.job);
setJob(props.job)
setSkills({
0: props.jobSkills[0],
1: props.jobSkills[1],
2: props.jobSkills[2],
3: props.jobSkills[3],
});
})
if (selectRef.current) selectRef.current.value = props.job.id;
if (selectRef.current) selectRef.current.value = props.job.id
}
}, [props]);
}, [props])
useEffect(() => {
generateImageUrl();
});
generateImageUrl()
})
useEffect(() => {
if (job) {
if ((party.job && job.id != party.job.id) || !party.job)
appState.party.job = job;
if (job.row === "1") setNumSkills(3);
else setNumSkills(4);
appState.party.job = job
if (job.row === '1') setNumSkills(3)
else setNumSkills(4)
}
}, [job]);
}, [job])
function receiveJob(job?: Job) {
if (job) {
setJob(job);
props.saveJob(job);
setJob(job)
props.saveJob(job)
}
}
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`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
}
setImageUrl(imgSrc);
setImageUrl(imgSrc)
}
const canEditSkill = (skill?: JobSkill) => {
if (job && skill) {
if (skill.job.id === job.id && skill.main && !skill.sub) return false;
if (skill.job.id === job.id && skill.main && !skill.sub) return false
}
return props.editable;
};
return props.editable
}
const skillItem = (index: number, editable: boolean) => {
return (
@ -103,15 +101,15 @@ 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'}
/>
);
};
)
}
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}
@ -119,17 +117,17 @@ const JobSection = (props: Props) => {
>
{skillItem(index, true)}
</SearchModal>
);
};
)
}
function saveJobSkill(object: SearchableObject, position: number) {
const skill = object as JobSkill;
const skill = object as JobSkill
const newSkills = skills;
newSkills[position] = skill;
setSkills(newSkills);
const newSkills = skills
newSkills[position] = skill
setSkills(newSkills)
props.saveSkill(skill, position);
props.saveSkill(skill, position)
}
// Render: JSX components
@ -161,7 +159,7 @@ const JobSection = (props: Props) => {
</ul>
</div>
</section>
);
};
)
}
export default JobSection;
export default JobSection

View file

@ -1,40 +1,40 @@
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"> {
skill?: JobSkill;
editable: boolean;
hasJob: boolean;
interface Props extends React.ComponentPropsWithoutRef<'div'> {
skill?: JobSkill
editable: boolean
hasJob: boolean
}
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
function useJobSkillItem({ ...props }, forwardedRef) {
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
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,
editable: props.editable,
});
})
const imageClasses = classNames({
placeholder: !props.skill,
editable: props.editable && props.hasJob,
});
})
const skillImage = () => {
let jsx: React.ReactNode;
let jsx: React.ReactNode
if (props.skill) {
jsx = (
@ -43,39 +43,39 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
className={imageClasses}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${props.skill.slug}.png`}
/>
);
)
} else {
jsx = (
<div className={imageClasses}>
{props.editable && props.hasJob ? <PlusIcon /> : ""}
{props.editable && props.hasJob ? <PlusIcon /> : ''}
</div>
);
)
}
return jsx;
};
return jsx
}
const label = () => {
let jsx: React.ReactNode;
let jsx: React.ReactNode
if (props.skill) {
jsx = <p>{props.skill.name[locale]}</p>;
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;
};
return jsx
}
return (
<div className={classes} onClick={props.onClick} ref={forwardedRef}>
{skillImage()}
{label()}
</div>
);
)
}
);
)
export default JobSkillItem;
export default JobSkillItem

View file

@ -1,43 +1,41 @@
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;
onClick: () => void;
data: JobSkill
onClick: () => void
}
const JobSkillResult = (props: Props) => {
const router = useRouter();
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;
const skill = props.data
const [group, setGroup] = useState<SkillGroup | undefined>();
const [group, setGroup] = useState<SkillGroup | undefined>()
useEffect(() => {
setGroup(skillClassification.find((group) => group.id === skill.color));
}, [skill, setGroup, skillClassification]);
setGroup(skillClassification.find((group) => group.id === skill.color))
}, [skill, setGroup, skillClassification])
const jobSkillUrl = () =>
`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${skill.slug}.png`;
`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${skill.slug}.png`
return (
<li className="JobSkillResult" onClick={props.onClick}>
<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>
</li>
);
};
)
}
export default JobSkillResult;
export default JobSkillResult

View file

@ -1,23 +1,23 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "react-i18next";
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'react-i18next'
import { skillGroups } from "~utils/skillGroups";
import { skillGroups } from '~utils/skillGroups'
import "./index.scss";
import './index.scss'
interface Props {
sendFilters: (filters: { [key: string]: number }) => void;
sendFilters: (filters: { [key: string]: number }) => void
}
const JobSkillSearchFilterBar = (props: Props) => {
// Set up translation
const { t } = useTranslation("common");
const { t } = useTranslation('common')
const [currentGroup, setCurrentGroup] = useState(-1);
const [currentGroup, setCurrentGroup] = useState(-1)
function onChange(event: React.ChangeEvent<HTMLSelectElement>) {
setCurrentGroup(parseInt(event.target.value));
setCurrentGroup(parseInt(event.target.value))
}
function onBlur(event: React.ChangeEvent<HTMLSelectElement>) {}
@ -25,14 +25,14 @@ const JobSkillSearchFilterBar = (props: Props) => {
function sendFilters() {
const filters = {
group: currentGroup,
};
}
props.sendFilters(filters);
props.sendFilters(filters)
}
useEffect(() => {
sendFilters();
}, [currentGroup]);
sendFilters()
}, [currentGroup])
return (
<div className="SearchFilterBar">
@ -65,7 +65,7 @@ const JobSkillSearchFilterBar = (props: Props) => {
</option>
</select>
</div>
);
};
)
}
export default JobSkillSearchFilterBar;
export default JobSkillSearchFilterBar

View file

@ -1,8 +1,8 @@
import type { ReactElement } from "react";
import TopHeader from "~components/TopHeader";
import type { ReactElement } from 'react'
import TopHeader from '~components/TopHeader'
interface Props {
children: ReactElement;
children: ReactElement
}
const Layout = ({ children }: Props) => {
@ -11,7 +11,7 @@ const Layout = ({ children }: Props) => {
<TopHeader />
<main>{children}</main>
</>
);
};
)
}
export default Layout;
export default Layout

View file

@ -1,138 +1,138 @@
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/Fieldset'
import CrossIcon from "~public/icons/Cross.svg";
import "./index.scss";
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
interface Props {}
interface ErrorMap {
[index: string]: string;
email: string;
password: string;
[index: string]: string
email: string
password: string
}
const emailRegex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const LoginModal = (props: Props) => {
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
const { t } = useTranslation('common')
// Set up form states and error handling
const [formValid, setFormValid] = useState(false);
const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({
email: "",
password: "",
});
email: '',
password: '',
})
// States
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(false)
// Set up form refs
const emailInput: React.RefObject<HTMLInputElement> = React.createRef();
const passwordInput: React.RefObject<HTMLInputElement> = React.createRef();
const form: React.RefObject<HTMLInputElement>[] = [emailInput, passwordInput];
const emailInput: React.RefObject<HTMLInputElement> = React.createRef()
const passwordInput: React.RefObject<HTMLInputElement> = React.createRef()
const form: React.RefObject<HTMLInputElement>[] = [emailInput, passwordInput]
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { name, value } = event.target;
let newErrors = { ...errors };
const { name, value } = event.target
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 = "";
break;
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") : "";
break;
value.length == 0 ? t('modals.login.errors.empty_password') : ''
break
default:
break;
break
}
setErrors(newErrors);
setFormValid(validateForm(newErrors));
setErrors(newErrors)
setFormValid(validateForm(newErrors))
}
function validateForm(errors: ErrorMap) {
let valid = true;
let valid = true
Object.values(form).forEach(
(input) => input.current?.value.length == 0 && (valid = false)
);
)
Object.values(errors).forEach(
(error) => error.length > 0 && (valid = false)
);
)
return valid;
return valid
}
function login(event: React.FormEvent) {
event.preventDefault();
event.preventDefault()
const body = {
email: emailInput.current?.value,
password: passwordInput.current?.value,
grant_type: "password",
};
grant_type: 'password',
}
if (formValid) {
api
.login(body)
.then((response) => {
storeCookieInfo(response);
return response.data.user.id;
storeCookieInfo(response)
return response.data.user.id
})
.then((id) => fetchUserInfo(id))
.then((infoResponse) => storeUserInfo(infoResponse));
.then((infoResponse) => storeUserInfo(infoResponse))
}
}
function fetchUserInfo(id: string) {
return api.userInfo(id);
return api.userInfo(id)
}
function storeCookieInfo(response: AxiosResponse) {
const user = response.data.user;
const user = response.data.user
const cookieObj: AccountCookie = {
userId: user.id,
username: user.username,
token: response.data.access_token,
};
}
setCookie("account", cookieObj, { path: "/" });
setCookie('account', cookieObj, { path: '/' })
}
function storeUserInfo(response: AxiosResponse) {
const user = response.data.user;
const user = response.data.user
const cookieObj: UserCookie = {
picture: user.picture.picture,
element: user.picture.element,
language: user.language,
gender: user.gender,
};
}
setCookie("user", cookieObj, { path: "/" });
setCookie('user', cookieObj, { path: '/' })
accountState.account.user = {
id: user.id,
@ -140,35 +140,35 @@ const LoginModal = (props: Props) => {
picture: user.picture.picture,
element: user.picture.element,
gender: user.gender,
};
}
console.log("Authorizing account...");
accountState.account.authorized = true;
console.log('Authorizing account...')
accountState.account.authorized = true
setOpen(false);
changeLanguage(user.language);
setOpen(false)
changeLanguage(user.language)
}
function changeLanguage(newLanguage: string) {
if (newLanguage !== router.locale) {
setCookie("NEXT_LOCALE", newLanguage, { path: "/" });
router.push(router.asPath, undefined, { locale: newLanguage });
setCookie('NEXT_LOCALE', newLanguage, { path: '/' })
router.push(router.asPath, undefined, { locale: newLanguage })
}
}
function openChange(open: boolean) {
setOpen(open);
setOpen(open)
setErrors({
email: "",
password: "",
});
email: '',
password: '',
})
}
return (
<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,19 +198,19 @@ 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" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default LoginModal;
export default LoginModal

View file

@ -1,53 +1,53 @@
import React from "react";
import { useSnapshot } from "valtio";
import { useTranslation } from "next-i18next";
import React from 'react'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import { appState } from "~utils/appState";
import { appState } from '~utils/appState'
import SegmentedControl from "~components/SegmentedControl";
import Segment from "~components/Segment";
import ToggleSwitch from "~components/ToggleSwitch";
import SegmentedControl from '~components/SegmentedControl'
import Segment from '~components/Segment'
import ToggleSwitch from '~components/ToggleSwitch'
import { GridType } from "~utils/enums";
import { GridType } from '~utils/enums'
import "./index.scss";
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;
let element: number = 0
if (party.element == 0 && grid.weapons.mainWeapon)
element = grid.weapons.mainWeapon.element;
else element = party.element;
element = grid.weapons.mainWeapon.element
else element = party.element
switch (element) {
case 1:
return "wind";
break;
return 'wind'
break
case 2:
return "fire";
break;
return 'fire'
break
case 3:
return "water";
break;
return 'water'
break
case 4:
return "earth";
break;
return 'earth'
break
case 5:
return "dark";
break;
return 'dark'
break
case 6:
return "light";
break;
return 'light'
break
}
}
@ -61,7 +61,7 @@ const PartySegmentedControl = (props: Props) => {
onChange={props.onCheckboxChange}
/>
</div>
);
)
return (
<div className="PartyNavigation">
@ -79,7 +79,7 @@ const PartySegmentedControl = (props: Props) => {
selected={props.selectedTab == GridType.Character}
onClick={props.onClick}
>
{t("party.segmented_control.characters")}
{t('party.segmented_control.characters')}
</Segment>
<Segment
@ -88,7 +88,7 @@ const PartySegmentedControl = (props: Props) => {
selected={props.selectedTab == GridType.Weapon}
onClick={props.onClick}
>
{t("party.segmented_control.weapons")}
{t('party.segmented_control.weapons')}
</Segment>
<Segment
@ -97,17 +97,17 @@ const PartySegmentedControl = (props: Props) => {
selected={props.selectedTab == GridType.Summon}
onClick={props.onClick}
>
{t("party.segmented_control.summons")}
{t('party.segmented_control.summons')}
</Segment>
</SegmentedControl>
{(() => {
if (party.editable && props.selectedTab == GridType.Weapon) {
return extraToggle;
return extraToggle
}
})()}
</div>
);
};
)
}
export default PartySegmentedControl;
export default PartySegmentedControl

View file

@ -1,16 +1,16 @@
import React from "react";
import React from 'react'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import ArrowIcon from "~public/icons/Arrow.svg";
import "./index.scss";
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) => {
@ -28,7 +28,7 @@ const SearchFilter = (props: Props) => {
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</DropdownMenu.Root>
);
};
)
}
export default SearchFilter;
export default SearchFilter

View file

@ -14,7 +14,7 @@
cursor: pointer;
}
&[data-state="checked"] {
&[data-state='checked'] {
background: $grey-90;
svg {

View file

@ -1,20 +1,20 @@
import React from "react";
import React from 'react'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import CheckIcon from "~public/icons/Check.svg";
import "./index.scss";
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);
props.onCheckedChange(checked, props.valueKey)
}
return (
@ -29,7 +29,7 @@ const SearchFilterCheckboxItem = (props: Props) => {
</DropdownMenu.ItemIndicator>
{props.children}
</DropdownMenu.CheckboxItem>
);
};
)
}
export default SearchFilterCheckboxItem;
export default SearchFilterCheckboxItem

View file

@ -1,70 +1,70 @@
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 * as Dialog from '@radix-ui/react-dialog'
import CharacterSearchFilterBar from "~components/CharacterSearchFilterBar";
import WeaponSearchFilterBar from "~components/WeaponSearchFilterBar";
import SummonSearchFilterBar from "~components/SummonSearchFilterBar";
import JobSkillSearchFilterBar from "~components/JobSkillSearchFilterBar";
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";
children: React.ReactNode;
send: (object: SearchableObject, position: number) => any
placeholderText: string
fromPosition: number
job?: Job
object: 'weapons' | 'characters' | 'summons' | 'job_skills'
children: React.ReactNode
}
const SearchModal = (props: Props) => {
// Set up router
const router = useRouter();
const locale = router.locale;
const router = useRouter()
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>();
let searchInput = React.createRef<HTMLInputElement>()
let scrollContainer = React.createRef<HTMLDivElement>()
const [firstLoad, setFirstLoad] = useState(true);
const [filters, setFilters] = useState<{ [key: string]: any }>();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchableObjectArray>([]);
const [firstLoad, setFirstLoad] = useState(true)
const [filters, setFilters] = useState<{ [key: string]: any }>()
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchableObjectArray>([])
// Pagination states
const [recordCount, setRecordCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [recordCount, setRecordCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
useEffect(() => {
if (searchInput.current) searchInput.current.focus();
}, [searchInput]);
if (searchInput.current) searchInput.current.focus()
}, [searchInput])
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
const text = event.target.value;
const text = event.target.value
if (text.length) {
setQuery(text);
setQuery(text)
} else {
setQuery("");
setQuery('')
}
}
@ -79,131 +79,131 @@ const SearchModal = (props: Props) => {
page: currentPage,
})
.then((response) => {
setTotalPages(response.data.total_pages);
setRecordCount(response.data.count);
setTotalPages(response.data.total_pages)
setRecordCount(response.data.count)
if (replace) {
replaceResults(response.data.count, response.data.results);
replaceResults(response.data.count, response.data.results)
} else {
appendResults(response.data.results);
appendResults(response.data.results)
}
})
.catch((error) => {
console.error(error);
});
console.error(error)
})
}
function replaceResults(count: number, list: SearchableObjectArray) {
if (count > 0) {
setResults(list);
setResults(list)
} else {
setResults([]);
setResults([])
}
}
function appendResults(list: SearchableObjectArray) {
setResults([...results, ...list]);
setResults([...results, ...list])
}
function storeRecentResult(result: SearchableObject) {
const key = `recent_${props.object}`;
const cookie = getCookie(key);
const key = `recent_${props.object}`
const cookie = getCookie(key)
const cookieObj: SearchableObjectArray = cookie
? JSON.parse(cookie as string)
: [];
let recents: SearchableObjectArray = [];
: []
let recents: SearchableObjectArray = []
if (props.object === "weapons") {
recents = cloneDeep(cookieObj as Weapon[]) || [];
if (props.object === 'weapons') {
recents = cloneDeep(cookieObj as Weapon[]) || []
if (
!recents.find(
(item) =>
(item as Weapon).granblue_id === (result as Weapon).granblue_id
)
) {
recents.unshift(result as Weapon);
recents.unshift(result as Weapon)
}
} else if (props.object === "summons") {
recents = cloneDeep(cookieObj as Summon[]) || [];
} else if (props.object === 'summons') {
recents = cloneDeep(cookieObj as Summon[]) || []
if (
!recents.find(
(item) =>
(item as Summon).granblue_id === (result as Summon).granblue_id
)
) {
recents.unshift(result as Summon);
recents.unshift(result as Summon)
}
}
if (recents && recents.length > 5) recents.pop();
setCookie(`recent_${props.object}`, recents, { path: "/" });
sendData(result);
if (recents && recents.length > 5) recents.pop()
setCookie(`recent_${props.object}`, recents, { path: '/' })
sendData(result)
}
function sendData(result: SearchableObject) {
props.send(result, props.fromPosition);
openChange();
props.send(result, props.fromPosition)
openChange()
}
function receiveFilters(filters: { [key: string]: any }) {
setCurrentPage(1);
setResults([]);
setFilters(filters);
setCurrentPage(1)
setResults([])
setFilters(filters)
}
useEffect(() => {
// Current page changed
if (open && currentPage > 1) {
fetchResults({ replace: false });
fetchResults({ replace: false })
} else if (open && currentPage == 1) {
fetchResults({ replace: true });
fetchResults({ replace: true })
}
}, [currentPage]);
}, [currentPage])
useEffect(() => {
// Filters changed
const key = `recent_${props.object}`;
const cookie = getCookie(key);
const key = `recent_${props.object}`
const cookie = getCookie(key)
const cookieObj: Weapon[] | Summon[] | Character[] = cookie
? JSON.parse(cookie as string)
: [];
: []
if (open) {
if (firstLoad && cookieObj && cookieObj.length > 0) {
setResults(cookieObj);
setRecordCount(cookieObj.length);
setFirstLoad(false);
setResults(cookieObj)
setRecordCount(cookieObj.length)
setFirstLoad(false)
} else {
setCurrentPage(1);
fetchResults({ replace: true });
setCurrentPage(1)
fetchResults({ replace: true })
}
}
}, [filters]);
}, [filters])
useEffect(() => {
// Query changed
if (open && query.length != 1) {
setCurrentPage(1);
fetchResults({ replace: true });
setCurrentPage(1)
fetchResults({ replace: true })
}
}, [query]);
}, [query])
function renderResults() {
let jsx;
let jsx
switch (props.object) {
case "weapons":
jsx = renderWeaponSearchResults();
break;
case "summons":
jsx = renderSummonSearchResults(results);
break;
case "characters":
jsx = renderCharacterSearchResults(results);
break;
case "job_skills":
jsx = renderJobSkillSearchResults(results);
break;
case 'weapons':
jsx = renderWeaponSearchResults()
break
case 'summons':
jsx = renderSummonSearchResults(results)
break
case 'characters':
jsx = renderCharacterSearchResults(results)
break
case 'job_skills':
jsx = renderJobSkillSearchResults(results)
break
}
return (
@ -216,13 +216,13 @@ const SearchModal = (props: Props) => {
>
{jsx}
</InfiniteScroll>
);
)
}
function renderWeaponSearchResults() {
let jsx: React.ReactNode;
let jsx: React.ReactNode
const castResults: Weapon[] = results as Weapon[];
const castResults: Weapon[] = results as Weapon[]
if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: Weapon) => {
return (
@ -230,20 +230,20 @@ const SearchModal = (props: Props) => {
key={result.id}
data={result}
onClick={() => {
storeRecentResult(result);
storeRecentResult(result)
}}
/>
);
});
)
})
}
return jsx;
return jsx
}
function renderSummonSearchResults(results: { [key: string]: any }) {
let jsx: React.ReactNode;
let jsx: React.ReactNode
const castResults: Summon[] = results as Summon[];
const castResults: Summon[] = results as Summon[]
if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: Summon) => {
return (
@ -251,20 +251,20 @@ const SearchModal = (props: Props) => {
key={result.id}
data={result}
onClick={() => {
storeRecentResult(result);
storeRecentResult(result)
}}
/>
);
});
)
})
}
return jsx;
return jsx
}
function renderCharacterSearchResults(results: { [key: string]: any }) {
let jsx: React.ReactNode;
let jsx: React.ReactNode
const castResults: Character[] = results as Character[];
const castResults: Character[] = results as Character[]
if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: Character) => {
return (
@ -272,20 +272,20 @@ const SearchModal = (props: Props) => {
key={result.id}
data={result}
onClick={() => {
storeRecentResult(result);
storeRecentResult(result)
}}
/>
);
});
)
})
}
return jsx;
return jsx
}
function renderJobSkillSearchResults(results: { [key: string]: any }) {
let jsx: React.ReactNode;
let jsx: React.ReactNode
const castResults: JobSkill[] = results as JobSkill[];
const castResults: JobSkill[] = results as JobSkill[]
if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: JobSkill) => {
return (
@ -293,26 +293,26 @@ const SearchModal = (props: Props) => {
key={result.id}
data={result}
onClick={() => {
storeRecentResult(result);
storeRecentResult(result)
}}
/>
);
});
)
})
}
return jsx;
return jsx
}
function openChange() {
if (open) {
setQuery("");
setFirstLoad(true);
setResults([]);
setRecordCount(0);
setCurrentPage(1);
setOpen(false);
setQuery('')
setFirstLoad(true)
setResults([])
setRecordCount(0)
setCurrentPage(1)
setOpen(false)
} else {
setOpen(true);
setOpen(true)
}
}
@ -340,39 +340,39 @@ const SearchModal = (props: Props) => {
<CrossIcon />
</Dialog.Close>
</div>
{props.object === "characters" ? (
{props.object === 'characters' ? (
<CharacterSearchFilterBar sendFilters={receiveFilters} />
) : (
""
''
)}
{props.object === "weapons" ? (
{props.object === 'weapons' ? (
<WeaponSearchFilterBar sendFilters={receiveFilters} />
) : (
""
''
)}
{props.object === "summons" ? (
{props.object === 'summons' ? (
<SummonSearchFilterBar sendFilters={receiveFilters} />
) : (
""
''
)}
{props.object === "job_skills" ? (
{props.object === 'job_skills' ? (
<JobSkillSearchFilterBar sendFilters={receiveFilters} />
) : (
""
''
)}
</div>
<div id="Results" ref={scrollContainer}>
<h5 className="total">
{t("search.result_count", { record_count: recordCount })}
{t('search.result_count', { record_count: recordCount })}
</h5>
{open ? renderResults() : ""}
{open ? renderResults() : ''}
</div>
</Dialog.Content>
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default SearchModal;
export default SearchModal

View file

@ -1,13 +1,13 @@
import React from "react";
import React from 'react'
import "./index.scss";
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) => {
@ -23,7 +23,7 @@ const Segment: React.FC<Props> = (props: Props) => {
/>
<label htmlFor={props.name}>{props.children}</label>
</div>
);
};
)
}
export default Segment;
export default Segment

View file

@ -1,19 +1,19 @@
import React from "react";
import React from 'react'
import "./index.scss";
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 : ""}`}>
<div className={`SegmentedControl ${elementClass ? elementClass : ''}`}>
{children}
</div>
</div>
);
};
)
}
export default SegmentedControl;
export default SegmentedControl

View file

@ -1,64 +1,64 @@
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/Fieldset'
import CrossIcon from "~public/icons/Cross.svg";
import "./index.scss";
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
interface Props {}
interface ErrorMap {
[index: string]: string;
username: string;
email: string;
password: string;
passwordConfirmation: string;
[index: string]: string
username: string
email: string
password: string
passwordConfirmation: string
}
const emailRegex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const SignupModal = (props: Props) => {
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
const { t } = useTranslation('common')
// Set up form states and error handling
const [formValid, setFormValid] = useState(false);
const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({
username: "",
email: "",
password: "",
passwordConfirmation: "",
});
username: '',
email: '',
password: '',
passwordConfirmation: '',
})
// States
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(false)
// Set up form refs
const usernameInput = React.createRef<HTMLInputElement>();
const emailInput = React.createRef<HTMLInputElement>();
const passwordInput = React.createRef<HTMLInputElement>();
const passwordConfirmationInput = React.createRef<HTMLInputElement>();
const usernameInput = React.createRef<HTMLInputElement>()
const emailInput = React.createRef<HTMLInputElement>()
const passwordInput = React.createRef<HTMLInputElement>()
const passwordConfirmationInput = React.createRef<HTMLInputElement>()
const form = [
usernameInput,
emailInput,
passwordInput,
passwordConfirmationInput,
];
]
function register(event: React.FormEvent) {
event.preventDefault();
event.preventDefault()
const body = {
user: {
@ -68,47 +68,47 @@ const SignupModal = (props: Props) => {
password_confirmation: passwordConfirmationInput.current?.value,
language: router.locale,
},
};
}
if (formValid)
api.endpoints.users
.create(body)
.then((response) => {
storeCookieInfo(response);
return response.data.user.user_id;
storeCookieInfo(response)
return response.data.user.user_id
})
.then((id) => fetchUserInfo(id))
.then((infoResponse) => storeUserInfo(infoResponse));
.then((infoResponse) => storeUserInfo(infoResponse))
}
function storeCookieInfo(response: AxiosResponse) {
const user = response.data.user;
const user = response.data.user
const cookieObj: AccountCookie = {
userId: user.user_id,
username: user.username,
token: user.token,
};
}
setCookie("account", cookieObj, { path: "/" });
setCookie('account', cookieObj, { path: '/' })
}
function fetchUserInfo(id: string) {
return api.userInfo(id);
return api.userInfo(id)
}
function storeUserInfo(response: AxiosResponse) {
const user = response.data.user;
const user = response.data.user
const cookieObj: UserCookie = {
picture: user.picture.picture,
element: user.picture.element,
language: user.language,
gender: user.gender,
};
}
// TODO: Set language
setCookie("user", cookieObj, { path: "/" });
setCookie('user', cookieObj, { path: '/' })
accountState.account.user = {
id: user.id,
@ -116,29 +116,29 @@ const SignupModal = (props: Props) => {
picture: user.picture.picture,
element: user.picture.element,
gender: user.gender,
};
}
accountState.account.authorized = true;
setOpen(false);
accountState.account.authorized = true
setOpen(false)
}
function handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
event.preventDefault()
const fieldName = event.target.name;
const value = event.target.value;
const fieldName = event.target.name
const value = event.target.value
if (value.length >= 3) {
api.check(fieldName, value).then(
(response) => {
processNameCheck(fieldName, value, response.data.available);
processNameCheck(fieldName, value, response.data.available)
},
(error) => {
console.error(error);
console.error(error)
}
);
)
} else {
validateName(fieldName, value);
validateName(fieldName, value)
}
}
@ -147,114 +147,114 @@ const SignupModal = (props: Props) => {
value: string,
available: boolean
) {
const newErrors = { ...errors };
const newErrors = { ...errors }
if (available) {
// Continue checking for errors
newErrors[fieldName] = "";
setErrors(newErrors);
setFormValid(true);
newErrors[fieldName] = ''
setErrors(newErrors)
setFormValid(true)
validateName(fieldName, value);
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);
setFormValid(false);
})
setErrors(newErrors)
setFormValid(false)
}
}
function validateName(fieldName: string, value: string) {
let newErrors = { ...errors };
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;
break
case "email":
case 'email':
newErrors.email = emailRegex.test(value)
? ""
: t("modals.signup.errors.invalid_email");
break;
? ''
: t('modals.signup.errors.invalid_email')
break
default:
break;
break
}
setFormValid(validateForm(newErrors));
setFormValid(validateForm(newErrors))
}
function handlePasswordChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
event.preventDefault()
const { name, value } = event.target;
let newErrors = { ...errors };
const { name, value } = event.target
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")
: "";
break;
? t('modals.signup.errors.password_contains_username')
: ''
break
case "password":
case 'password':
newErrors.password =
value.length < 8 ? t("modals.signup.errors.password_too_short") : "";
break;
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");
break;
? ''
: t('modals.signup.errors.passwords_dont_match')
break
default:
break;
break
}
setFormValid(validateForm(newErrors));
setFormValid(validateForm(newErrors))
}
function validateForm(errors: ErrorMap) {
let valid = true;
let valid = true
Object.values(form).forEach(
(input) => input.current?.value.length == 0 && (valid = false)
);
)
Object.values(errors).forEach(
(error) => error.length > 0 && (valid = false)
);
)
return valid;
return valid
}
function openChange(open: boolean) {
setOpen(open);
setOpen(open)
setErrors({
username: "",
email: "",
password: "",
passwordConfirmation: "",
});
username: '',
email: '',
password: '',
passwordConfirmation: '',
})
}
return (
<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">
@ -318,7 +318,7 @@ const SignupModal = (props: Props) => {
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default SignupModal;
export default SignupModal

View file

@ -1,53 +1,53 @@
/* 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 {
new: boolean;
summons?: GridSummon[];
createParty: () => Promise<AxiosResponse<any, any>>;
pushHistory?: (path: string) => void;
new: boolean
summons?: GridSummon[]
createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void
}
const SummonGrid = (props: Props) => {
// Constants
const numSummons: number = 4;
const numSummons: number = 4
// Cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {};
: {}
// Localization
const { t } = useTranslation("common");
const { t } = useTranslation('common')
// Set up state for view management
const { party, grid } = useSnapshot(appState);
const [slug, setSlug] = useState();
const { party, grid } = useSnapshot(appState)
const [slug, setSlug] = useState()
// Create a temporary state to store previous weapon uncap value
const [previousUncapValues, setPreviousUncapValues] = useState<{
[key: number]: number;
}>({});
[key: number]: number
}>({})
// Set the editable flag only on first load
useEffect(() => {
@ -56,61 +56,61 @@ const SummonGrid = (props: Props) => {
(accountData && party.user && accountData.userId === party.user.id) ||
props.new
)
appState.party.editable = true;
else appState.party.editable = false;
}, [props.new, accountData, party]);
appState.party.editable = true
else appState.party.editable = false
}, [props.new, accountData, party])
// Initialize an array of current uncap values for each summon
useEffect(() => {
let initialPreviousUncapValues: { [key: number]: number } = {};
let initialPreviousUncapValues: { [key: number]: number } = {}
if (appState.grid.summons.mainSummon)
initialPreviousUncapValues[-1] =
appState.grid.summons.mainSummon.uncap_level;
appState.grid.summons.mainSummon.uncap_level
if (appState.grid.summons.friendSummon)
initialPreviousUncapValues[6] =
appState.grid.summons.friendSummon.uncap_level;
appState.grid.summons.friendSummon.uncap_level
Object.values(appState.grid.summons.allSummons).map((o) =>
o ? (initialPreviousUncapValues[o.position] = o.uncap_level) : 0
);
)
setPreviousUncapValues(initialPreviousUncapValues);
setPreviousUncapValues(initialPreviousUncapValues)
}, [
appState.grid.summons.mainSummon,
appState.grid.summons.friendSummon,
appState.grid.summons.allSummons,
]);
])
// Methods: Adding an object from search
function receiveSummonFromSearch(object: SearchableObject, position: number) {
const summon = object as Summon;
const summon = object as Summon
if (!party.id) {
props.createParty().then((response) => {
const party = response.data.party;
appState.party.id = party.id;
setSlug(party.shortcode);
const party = response.data.party
appState.party.id = party.id
setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`);
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveSummon(party.id, summon, position).then((response) =>
storeGridSummon(response.data.grid_summon)
);
});
)
})
} else {
if (party.editable)
saveSummon(party.id, summon, position).then((response) =>
storeGridSummon(response.data.grid_summon)
);
)
}
}
async function saveSummon(partyId: string, summon: Summon, position: number) {
let uncapLevel = 3;
if (summon.uncap.ulb) uncapLevel = 5;
else if (summon.uncap.flb) uncapLevel = 4;
let uncapLevel = 3
if (summon.uncap.ulb) uncapLevel = 5
else if (summon.uncap.flb) uncapLevel = 4
return await api.endpoints.summons.create(
{
@ -124,37 +124,36 @@ const SummonGrid = (props: Props) => {
},
},
headers
);
)
}
function storeGridSummon(gridSummon: GridSummon) {
if (gridSummon.position == -1)
appState.grid.summons.mainSummon = gridSummon;
if (gridSummon.position == -1) appState.grid.summons.mainSummon = gridSummon
else if (gridSummon.position == 6)
appState.grid.summons.friendSummon = gridSummon;
else appState.grid.summons.allSummons[gridSummon.position] = gridSummon;
appState.grid.summons.friendSummon = gridSummon
else appState.grid.summons.allSummons[gridSummon.position] = gridSummon
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
storePreviousUncapValue(position);
storePreviousUncapValue(position)
try {
if (uncapLevel != previousUncapValues[position])
await api.updateUncap("summon", id, uncapLevel).then((response) => {
storeGridSummon(response.data.grid_summon);
});
await api.updateUncap('summon', id, uncapLevel).then((response) => {
storeGridSummon(response.data.grid_summon)
})
} catch (error) {
console.error(error);
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position]);
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = { ...previousUncapValues };
delete newPreviousValues[position];
setPreviousUncapValues(newPreviousValues);
let newPreviousValues = { ...previousUncapValues }
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
@ -163,63 +162,62 @@ const SummonGrid = (props: Props) => {
position: number,
uncapLevel: number
) {
memoizeAction(id, position, uncapLevel);
memoizeAction(id, position, uncapLevel)
// Optimistically update UI
updateUncapLevel(position, uncapLevel);
updateUncapLevel(position, uncapLevel)
}
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel);
debouncedAction(id, position, uncapLevel)
},
[props, previousUncapValues]
);
)
const debouncedAction = useMemo(
() =>
debounce((id, position, number) => {
saveUncap(id, position, number);
saveUncap(id, position, number)
}, 500),
[props, saveUncap]
);
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
if (appState.grid.summons.mainSummon && position == -1)
appState.grid.summons.mainSummon.uncap_level = uncapLevel;
appState.grid.summons.mainSummon.uncap_level = uncapLevel
else if (appState.grid.summons.friendSummon && position == 6)
appState.grid.summons.friendSummon.uncap_level = uncapLevel;
appState.grid.summons.friendSummon.uncap_level = uncapLevel
else {
const summon = appState.grid.summons.allSummons[position];
const summon = appState.grid.summons.allSummons[position]
if (summon) {
summon.uncap_level = uncapLevel;
appState.grid.summons.allSummons[position] = summon;
summon.uncap_level = uncapLevel
appState.grid.summons.allSummons[position] = summon
}
}
}
};
function storePreviousUncapValue(position: number) {
// Save the current value in case of an unexpected result
let newPreviousValues = { ...previousUncapValues };
let newPreviousValues = { ...previousUncapValues }
if (appState.grid.summons.mainSummon && position == -1)
newPreviousValues[position] =
appState.grid.summons.mainSummon.uncap_level;
newPreviousValues[position] = appState.grid.summons.mainSummon.uncap_level
else if (appState.grid.summons.friendSummon && position == 6)
newPreviousValues[position] =
appState.grid.summons.friendSummon.uncap_level;
appState.grid.summons.friendSummon.uncap_level
else {
const summon = appState.grid.summons.allSummons[position];
newPreviousValues[position] = summon ? summon.uncap_level : 0;
const summon = appState.grid.summons.allSummons[position]
newPreviousValues[position] = summon ? summon.uncap_level : 0
}
setPreviousUncapValues(newPreviousValues);
setPreviousUncapValues(newPreviousValues)
}
// 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}
@ -230,11 +228,11 @@ const SummonGrid = (props: Props) => {
updateUncap={initiateUncapUpdate}
/>
</div>
);
)
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}
@ -245,10 +243,10 @@ const SummonGrid = (props: Props) => {
updateUncap={initiateUncapUpdate}
/>
</div>
);
)
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 (
@ -262,11 +260,11 @@ const SummonGrid = (props: Props) => {
updateUncap={initiateUncapUpdate}
/>
</li>
);
)
})}
</ul>
</div>
);
)
const subAuraSummonElement = (
<ExtraSummons
grid={grid.summons.allSummons}
@ -276,7 +274,7 @@ const SummonGrid = (props: Props) => {
updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate}
/>
);
)
return (
<div>
<div id="SummonGrid">
@ -287,7 +285,7 @@ const SummonGrid = (props: Props) => {
{subAuraSummonElement}
</div>
);
};
)
}
export default SummonGrid;
export default SummonGrid

View file

@ -1,64 +1,62 @@
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 * as HoverCard from "@radix-ui/react-hover-card";
import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import "./index.scss";
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 router = useRouter()
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 Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const tintElement = Element[props.gridSummon.object.element];
const tintElement = Element[props.gridSummon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(
" ",
"_"
)}`;
' ',
'_'
)}`
function summonImage() {
let imgSrc = "";
let imgSrc = ''
if (props.gridSummon) {
const summon = props.gridSummon.object;
const summon = props.gridSummon.object
const upgradedSummons = [
"2040094000",
"2040100000",
"2040080000",
"2040098000",
"2040090000",
"2040084000",
"2040003000",
"2040056000",
];
'2040094000',
'2040100000',
'2040080000',
'2040098000',
'2040090000',
'2040084000',
'2040003000',
'2040056000',
]
let suffix = "";
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
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
}
return imgSrc;
return imgSrc
}
return (
@ -88,12 +86,12 @@ const SummonHovercard = (props: Props) => {
</div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t("buttons.wiki")}
{t('buttons.wiki')}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
);
};
)
}
export default SummonHovercard;
export default SummonHovercard

View file

@ -1,26 +1,24 @@
import React from "react";
import { useRouter } from "next/router";
import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import "./index.scss";
import './index.scss'
interface Props {
data: Summon;
onClick: () => void;
data: Summon
onClick: () => void
}
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const SummonResult = (props: Props) => {
const router = useRouter();
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 summon = props.data;
const summon = props.data
return (
<li className="SummonResult" onClick={props.onClick}>
@ -41,7 +39,7 @@ const SummonResult = (props: Props) => {
</div>
</div>
</li>
);
};
)
}
export default SummonResult;
export default SummonResult

View file

@ -1,81 +1,81 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import cloneDeep from "lodash.clonedeep";
import cloneDeep from 'lodash.clonedeep'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import SearchFilter from "~components/SearchFilter";
import SearchFilterCheckboxItem from "~components/SearchFilterCheckboxItem";
import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import "./index.scss";
import { emptyElementState, emptyRarityState } from "~utils/emptyStates";
import { elements, rarities } from "~utils/stateValues";
import './index.scss'
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 [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] =
useState<ElementState>(emptyElementState);
useState<ElementState>(emptyElementState)
function rarityMenuOpened(open: boolean) {
if (open) {
setRarityMenu(true);
setElementMenu(false);
} else setRarityMenu(false);
setRarityMenu(true)
setElementMenu(false)
} else setRarityMenu(false)
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(true);
} else setElementMenu(false);
setRarityMenu(false)
setElementMenu(true)
} else setElementMenu(false)
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState);
newRarityState[key].checked = checked;
setRarityState(newRarityState);
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);
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);
.map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
};
}
props.sendFilters(filters);
props.sendFilters(filters)
}
useEffect(() => {
sendFilters();
}, [rarityState, elementState]);
sendFilters()
}, [rarityState, elementState])
return (
<div className="SearchFilterBar">
<SearchFilter
label={t("filters.labels.rarity")}
label={t('filters.labels.rarity')}
numSelected={
Object.values(rarityState)
.map((x) => x.checked)
@ -85,7 +85,7 @@ const SummonSearchFilterBar = (props: Props) => {
onOpenChange={rarityMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.rarity")}
{t('filters.labels.rarity')}
</DropdownMenu.Label>
{Array.from(Array(rarities.length)).map((x, i) => {
return (
@ -97,12 +97,12 @@ const SummonSearchFilterBar = (props: Props) => {
>
{t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</SearchFilter>
<SearchFilter
label={t("filters.labels.element")}
label={t('filters.labels.element')}
numSelected={
Object.values(elementState)
.map((x) => x.checked)
@ -112,7 +112,7 @@ const SummonSearchFilterBar = (props: Props) => {
onOpenChange={elementMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.element")}
{t('filters.labels.element')}
</DropdownMenu.Label>
{Array.from(Array(elements.length)).map((x, i) => {
return (
@ -124,11 +124,11 @@ const SummonSearchFilterBar = (props: Props) => {
>
{t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</SearchFilter>
</div>
);
};
)
}
export default SummonSearchFilterBar;
export default SummonSearchFilterBar

View file

@ -1,36 +1,34 @@
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;
unitType: 0 | 1 | 2;
position: number;
editable: boolean;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
gridSummon: GridSummon | undefined
unitType: 0 | 1 | 2
position: number
editable: boolean
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const SummonUnit = (props: Props) => {
const { t } = useTranslation("common");
const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState("");
const [imageUrl, setImageUrl] = useState('')
const router = useRouter();
const router = useRouter()
const locale =
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,
@ -39,57 +37,57 @@ const SummonUnit = (props: Props) => {
friend: props.unitType == 2,
editable: props.editable,
filled: props.gridSummon !== undefined,
});
})
const gridSummon = props.gridSummon;
const summon = gridSummon?.object;
const gridSummon = props.gridSummon
const summon = gridSummon?.object
useEffect(() => {
generateImageUrl();
});
generateImageUrl()
})
function generateImageUrl() {
let imgSrc = "";
let imgSrc = ''
if (props.gridSummon) {
const summon = props.gridSummon.object!;
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)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
}
setImageUrl(imgSrc);
setImageUrl(imgSrc)
}
function passUncapData(uncap: number) {
if (props.gridSummon)
props.updateUncap(props.gridSummon.id, props.position, uncap);
props.updateUncap(props.gridSummon.id, props.position, uncap)
}
const image = (
@ -100,21 +98,21 @@ 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}
>
{image}
</SearchModal>
);
)
const unitContent = (
<div className={classes}>
@ -129,17 +127,17 @@ const SummonUnit = (props: Props) => {
special={false}
/>
) : (
""
''
)}
<h3 className="SummonName">{summon?.name[locale]}</h3>
</div>
);
)
const withHovercard = (
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
);
)
return gridSummon && !props.editable ? withHovercard : unitContent;
};
return gridSummon && !props.editable ? withHovercard : unitContent
}
export default SummonUnit;
export default SummonUnit

View file

@ -1,6 +1,6 @@
.Fieldset textarea {
color: $grey-10;
font-family: system-ui, -apple-system, "Helvetica Neue", Helvetica, Arial,
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial,
sans-serif;
line-height: 21px;
}

View file

@ -1,13 +1,13 @@
import React from "react";
import "./index.scss";
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>(
@ -18,15 +18,15 @@ const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(
className="Input"
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ""}
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,12 +1,12 @@
import React from "react";
import React from 'react'
import "./index.scss";
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) => {
@ -25,7 +25,7 @@ const ToggleSwitch: React.FC<Props> = (props: Props) => {
<span className="toggle-switch-switch" />
</label>
</div>
);
};
)
}
export default ToggleSwitch;
export default ToggleSwitch

View file

@ -1,103 +1,103 @@
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'
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 = {};
const headers = {}
// accountCookies.account != null
// ? {
// Authorization: `Bearer ${accountCookies.account.access_token}`,
// }
// : {}
const { account } = useSnapshot(accountState);
const { party } = useSnapshot(appState);
const router = useRouter();
const { account } = useSnapshot(accountState)
const { party } = useSnapshot(appState)
const router = useRouter()
function copyToClipboard() {
const el = document.createElement("input");
el.value = window.location.href;
el.id = "url-input";
document.body.appendChild(el);
const el = document.createElement('input')
el.value = window.location.href
el.id = 'url-input'
document.body.appendChild(el)
el.select();
document.execCommand("copy");
el.remove();
el.select()
document.execCommand('copy')
el.remove()
}
function newParty() {
// Push the root URL
router.push("/");
router.push('/')
// Clean state
const resetState = clonedeep(initialAppState);
const resetState = clonedeep(initialAppState)
Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key];
});
appState[key] = resetState[key]
})
// Set party to be editable
appState.party.editable = true;
appState.party.editable = true
}
function logout() {
deleteCookie("account");
deleteCookie("user");
deleteCookie('account')
deleteCookie('user')
// Clean state
const resetState = clonedeep(initialAccountState);
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("/");
return false;
router.push('/')
return false
}
function toggleFavorite() {
if (party.favorited) unsaveFavorite();
else saveFavorite();
if (party.favorited) unsaveFavorite()
else saveFavorite()
}
function saveFavorite() {
if (party.id)
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");
if (response.status == 201) appState.party.favorited = true
})
else console.error('Failed to save team: No party ID')
}
function unsaveFavorite() {
if (party.id)
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");
if (response.status == 200) appState.party.favorited = false
})
else console.error('Failed to unsave team: No party ID')
}
const leftNav = () => {
return (
<div className="dropdown">
<Button icon="menu">{t("buttons.menu")}</Button>
<Button icon="menu">{t('buttons.menu')}</Button>
{account.user ? (
<HeaderMenu
authenticated={account.authorized}
@ -108,8 +108,8 @@ const TopHeader = () => {
<HeaderMenu authenticated={account.authorized} />
)}
</div>
);
};
)
}
const saveButton = () => {
if (party.favorited)
@ -117,38 +117,38 @@ const TopHeader = () => {
<Button icon="save" active={true} onClick={toggleFavorite}>
Saved
</Button>
);
)
else
return (
<Button icon="save" onClick={toggleFavorite}>
Save
</Button>
);
};
)
}
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]" ? (
: ''}
{router.route === '/p/[party]' ? (
<Button icon="link" onClick={copyToClipboard}>
{t("buttons.copy")}
{t('buttons.copy')}
</Button>
) : (
""
''
)}
<Button icon="new" onClick={newParty}>
{t("buttons.new")}
{t('buttons.new')}
</Button>
</div>
);
};
)
}
return <Header position="top" left={leftNav()} right={rightNav()} />;
};
return <Header position="top" left={leftNav()} right={rightNav()} />
}
export default TopHeader;
export default TopHeader

View file

@ -1,60 +1,60 @@
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";
rarity?: number;
uncapLevel?: number;
flb: boolean;
ulb: boolean;
special: boolean;
updateUncap?: (uncap: number) => void;
type: 'character' | 'weapon' | 'summon'
rarity?: number
uncapLevel?: number
flb: boolean
ulb: boolean
special: boolean
updateUncap?: (uncap: number) => void
}
const UncapIndicator = (props: Props) => {
const [uncap, setUncap] = useState(props.uncapLevel);
const [uncap, setUncap] = useState(props.uncapLevel)
const numStars = setNumStars();
const numStars = setNumStars()
function setNumStars() {
let numStars;
let numStars
if (props.type === "character") {
if (props.type === 'character') {
if (props.special) {
if (props.ulb) {
numStars = 5;
numStars = 5
} else if (props.flb) {
numStars = 4;
numStars = 4
} else {
numStars = 3;
numStars = 3
}
} else {
if (props.ulb) {
numStars = 6;
numStars = 6
} else if (props.flb) {
numStars = 5;
numStars = 5
} else {
numStars = 4;
numStars = 4
}
}
} else {
if (props.ulb) {
numStars = 5;
numStars = 5
} else if (props.flb) {
numStars = 4;
numStars = 4
} else {
numStars = 3;
numStars = 3
}
}
return numStars;
return numStars
}
function toggleStar(index: number, empty: boolean) {
if (props.updateUncap) {
if (empty) props.updateUncap(index + 1);
else props.updateUncap(index);
if (empty) props.updateUncap(index + 1)
else props.updateUncap(index)
}
}
@ -67,8 +67,8 @@ const UncapIndicator = (props: Props) => {
index={i}
onClick={toggleStar}
/>
);
};
)
}
const ulb = (i: number) => {
return (
@ -79,8 +79,8 @@ const UncapIndicator = (props: Props) => {
index={i}
onClick={toggleStar}
/>
);
};
)
}
const flb = (i: number) => {
return (
@ -91,8 +91,8 @@ const UncapIndicator = (props: Props) => {
index={i}
onClick={toggleStar}
/>
);
};
)
}
const mlb = (i: number) => {
// console.log("MLB; Number of stars:", props.uncapLevel)
@ -103,27 +103,27 @@ const UncapIndicator = (props: Props) => {
index={i}
onClick={toggleStar}
/>
);
};
)
}
return (
<ul className="UncapIndicator">
{Array.from(Array(numStars)).map((x, i) => {
if (props.type === "character" && i > 4) {
if (props.special) return ulb(i);
else return transcendence(i);
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);
return flb(i)
} else {
return mlb(i);
return mlb(i)
}
})}
</ul>
);
};
)
}
export default UncapIndicator;
export default UncapIndicator

View file

@ -14,42 +14,42 @@
&.empty.flb,
&.empty.ulb,
&.empty.special {
background: url("/icons/uncap/empty.svg");
background: url('/icons/uncap/empty.svg');
&:hover {
background: url("/icons/uncap/empty-hover.svg");
background: url('/icons/uncap/empty-hover.svg');
}
}
&.mlb {
background: url("/icons/uncap/yellow.svg");
background: url('/icons/uncap/yellow.svg');
&:hover {
background: url("/icons/uncap/yellow-hover.svg");
background: url('/icons/uncap/yellow-hover.svg');
}
}
&.special {
background: url("/icons/uncap/red.svg");
background: url('/icons/uncap/red.svg');
&:hover {
background: url("/icons/uncap/red-hover.svg");
background: url('/icons/uncap/red-hover.svg');
}
}
&.flb {
background: url("/icons/uncap/blue.svg");
background: url('/icons/uncap/blue.svg');
&:hover {
background: url("/icons/uncap/blue-hover.svg");
background: url('/icons/uncap/blue-hover.svg');
}
}
&.ulb {
background: url("/icons/uncap/purple.svg");
background: url('/icons/uncap/purple.svg');
&:hover {
background: url("/icons/uncap/purple-hover.svg");
background: url('/icons/uncap/purple-hover.svg');
}
}
}

View file

@ -1,15 +1,15 @@
import React from "react";
import classnames from "classnames";
import React from 'react'
import classnames from 'classnames'
import "./index.scss";
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) => {
@ -20,20 +20,20 @@ const UncapStar = (props: Props) => {
mlb: !props.special,
flb: props.flb,
ulb: props.ulb,
});
})
function clicked() {
props.onClick(props.index, props.empty);
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,
};
}
export default UncapStar;
export default UncapStar

View file

@ -1,50 +1,50 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { getCookie } from "cookies-next";
import { useSnapshot } from "valtio";
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { getCookie } from 'cookies-next'
import { useSnapshot } from 'valtio'
import { AxiosResponse } from "axios";
import debounce from "lodash.debounce";
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
import WeaponUnit from "~components/WeaponUnit";
import ExtraWeapons from "~components/ExtraWeapons";
import WeaponUnit from '~components/WeaponUnit'
import ExtraWeapons from '~components/ExtraWeapons'
import api from "~utils/api";
import { appState } from "~utils/appState";
import api from '~utils/api'
import { appState } from '~utils/appState'
import type { SearchableObject } from "~types";
import type { SearchableObject } from '~types'
import "./index.scss";
import './index.scss'
// Props
interface Props {
new: boolean;
weapons?: GridWeapon[];
createParty: (extra: boolean) => Promise<AxiosResponse<any, any>>;
pushHistory?: (path: string) => void;
new: boolean
weapons?: GridWeapon[]
createParty: (extra: boolean) => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void
}
const WeaponGrid = (props: Props) => {
// Constants
const numWeapons: number = 9;
const numWeapons: number = 9
// Cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {};
: {}
// Set up state for view management
const { party, grid } = useSnapshot(appState);
const [slug, setSlug] = useState();
const { party, grid } = useSnapshot(appState)
const [slug, setSlug] = useState()
// Create a temporary state to store previous weapon uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{
[key: number]: number;
}>({});
[key: number]: number
}>({})
// Set the editable flag only on first load
useEffect(() => {
@ -53,53 +53,53 @@ const WeaponGrid = (props: Props) => {
(accountData && party.user && accountData.userId === party.user.id) ||
props.new
)
appState.party.editable = true;
else appState.party.editable = false;
}, [props.new, accountData, party]);
appState.party.editable = true
else appState.party.editable = false
}, [props.new, accountData, party])
// Initialize an array of current uncap values for each weapon
useEffect(() => {
let initialPreviousUncapValues: { [key: number]: number } = {};
let initialPreviousUncapValues: { [key: number]: number } = {}
if (appState.grid.weapons.mainWeapon)
initialPreviousUncapValues[-1] =
appState.grid.weapons.mainWeapon.uncap_level;
appState.grid.weapons.mainWeapon.uncap_level
Object.values(appState.grid.weapons.allWeapons).map((o) =>
o ? (initialPreviousUncapValues[o.position] = o.uncap_level) : 0
);
)
setPreviousUncapValues(initialPreviousUncapValues);
}, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons]);
setPreviousUncapValues(initialPreviousUncapValues)
}, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons])
// Methods: Adding an object from search
function receiveWeaponFromSearch(object: SearchableObject, position: number) {
const weapon = object as Weapon;
if (position == 1) appState.party.element = weapon.element;
const weapon = object as Weapon
if (position == 1) appState.party.element = weapon.element
if (!party.id) {
props.createParty(party.extra).then((response) => {
const party = response.data.party;
appState.party.id = party.id;
setSlug(party.shortcode);
const party = response.data.party
appState.party.id = party.id
setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`);
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveWeapon(party.id, weapon, position).then((response) =>
storeGridWeapon(response.data.grid_weapon)
);
});
)
})
} else {
saveWeapon(party.id, weapon, position).then((response) =>
storeGridWeapon(response.data.grid_weapon)
);
)
}
}
async function saveWeapon(partyId: string, weapon: Weapon, position: number) {
let uncapLevel = 3;
if (weapon.uncap.ulb) uncapLevel = 5;
else if (weapon.uncap.flb) uncapLevel = 4;
let uncapLevel = 3
if (weapon.uncap.ulb) uncapLevel = 5
else if (weapon.uncap.flb) uncapLevel = 4
return await api.endpoints.weapons.create(
{
@ -112,39 +112,39 @@ const WeaponGrid = (props: Props) => {
},
},
headers
);
)
}
function storeGridWeapon(gridWeapon: GridWeapon) {
if (gridWeapon.position == -1) {
appState.grid.weapons.mainWeapon = gridWeapon;
appState.party.element = gridWeapon.object.element;
appState.grid.weapons.mainWeapon = gridWeapon
appState.party.element = gridWeapon.object.element
} else {
// Store the grid unit at the correct position
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon;
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
}
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
storePreviousUncapValue(position);
storePreviousUncapValue(position)
try {
if (uncapLevel != previousUncapValues[position])
await api.updateUncap("weapon", id, uncapLevel).then((response) => {
storeGridWeapon(response.data.grid_weapon);
});
await api.updateUncap('weapon', id, uncapLevel).then((response) => {
storeGridWeapon(response.data.grid_weapon)
})
} catch (error) {
console.error(error);
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position]);
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = { ...previousUncapValues };
delete newPreviousValues[position];
setPreviousUncapValues(newPreviousValues);
let newPreviousValues = { ...previousUncapValues }
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
@ -153,56 +153,55 @@ const WeaponGrid = (props: Props) => {
position: number,
uncapLevel: number
) {
memoizeAction(id, position, uncapLevel);
memoizeAction(id, position, uncapLevel)
// Optimistically update UI
updateUncapLevel(position, uncapLevel);
updateUncapLevel(position, uncapLevel)
}
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel);
debouncedAction(id, position, uncapLevel)
},
[props, previousUncapValues]
);
)
const debouncedAction = useMemo(
() =>
debounce((id, position, number) => {
saveUncap(id, position, number);
saveUncap(id, position, number)
}, 500),
[props, saveUncap]
);
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
if (appState.grid.weapons.mainWeapon && position == -1)
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel;
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel
else {
const weapon = appState.grid.weapons.allWeapons[position];
const weapon = appState.grid.weapons.allWeapons[position]
if (weapon) {
weapon.uncap_level = uncapLevel;
appState.grid.weapons.allWeapons[position] = weapon;
weapon.uncap_level = uncapLevel
appState.grid.weapons.allWeapons[position] = weapon
}
}
}
};
function storePreviousUncapValue(position: number) {
// Save the current value in case of an unexpected result
let newPreviousValues = { ...previousUncapValues };
let newPreviousValues = { ...previousUncapValues }
if (appState.grid.weapons.mainWeapon && position == -1) {
newPreviousValues[position] =
appState.grid.weapons.mainWeapon.uncap_level;
newPreviousValues[position] = appState.grid.weapons.mainWeapon.uncap_level
} else {
const weapon = appState.grid.weapons.allWeapons[position];
const weapon = appState.grid.weapons.allWeapons[position]
if (weapon) {
newPreviousValues[position] = weapon.uncap_level;
newPreviousValues[position] = weapon.uncap_level
} else {
newPreviousValues[position] = 0;
newPreviousValues[position] = 0
}
}
setPreviousUncapValues(newPreviousValues);
setPreviousUncapValues(newPreviousValues)
}
// Render: JSX components
@ -216,7 +215,7 @@ const WeaponGrid = (props: Props) => {
updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate}
/>
);
)
const weaponGridElement = Array.from(Array(numWeapons)).map((x, i) => {
return (
@ -230,8 +229,8 @@ const WeaponGrid = (props: Props) => {
updateUncap={initiateUncapUpdate}
/>
</li>
);
});
)
})
const extraGridElement = (
<ExtraWeapons
@ -241,7 +240,7 @@ const WeaponGrid = (props: Props) => {
updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate}
/>
);
)
return (
<div id="WeaponGrid">
@ -251,10 +250,10 @@ const WeaponGrid = (props: Props) => {
</div>
{(() => {
return party.extra ? extraGridElement : "";
return party.extra ? extraGridElement : ''
})()}
</div>
);
};
)
}
export default WeaponGrid;
export default WeaponGrid

View file

@ -1,134 +1,132 @@
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 * as HoverCard from "@radix-ui/react-hover-card";
import * as HoverCard from '@radix-ui/react-hover-card'
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import { axData } from "~utils/axData";
import { axData } from '~utils/axData'
import "./index.scss";
import './index.scss'
interface Props {
gridWeapon: GridWeapon;
children: React.ReactNode;
gridWeapon: GridWeapon
children: React.ReactNode
}
interface KeyNames {
[key: string]: {
[key: string]: string;
en: string;
ja: string;
};
[key: string]: string
en: string
ja: string
}
}
const WeaponHovercard = (props: Props) => {
const router = useRouter();
const { t } = useTranslation("common");
const router = useRouter()
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 Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = [
"none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const WeaponKeyNames: KeyNames = {
"2": {
en: "Pendulum",
ja: "ペンデュラム",
'2': {
en: 'Pendulum',
ja: 'ペンデュラム',
},
"3": {
en: "Teluma",
ja: "テルマ",
'3': {
en: 'Teluma',
ja: 'テルマ',
},
"17": {
en: "Gauph Key",
ja: "ガフスキー",
'17': {
en: 'Gauph Key',
ja: 'ガフスキー',
},
"22": {
en: "Emblem",
ja: "エンブレム",
'22': {
en: 'Emblem',
ja: 'エンブレム',
},
};
}
const tintElement =
props.gridWeapon.object.element == 0 && props.gridWeapon.element
? Element[props.gridWeapon.element]
: Element[props.gridWeapon.object.element];
: Element[props.gridWeapon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(
" ",
"_"
)}`;
' ',
'_'
)}`
const hovercardSide = () => {
if (props.gridWeapon.position == -1) return "right";
if (props.gridWeapon.position == -1) return 'right'
else if ([6, 7, 8, 9, 10, 11].includes(props.gridWeapon.position))
return "top";
else return "bottom";
};
const createPrimaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1];
if (props.gridWeapon.ax) {
const simpleAxSkill = props.gridWeapon.ax[0];
const axSkill = primaryAxSkills.find(
(skill) => skill.id == simpleAxSkill.modifier
);
return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${
axSkill?.suffix ? axSkill.suffix : ""
}`;
return 'top'
else return 'bottom'
}
return "";
};
const createSecondaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1];
const createPrimaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1]
if (props.gridWeapon.ax) {
const primarySimpleAxSkill = props.gridWeapon.ax[0];
const secondarySimpleAxSkill = props.gridWeapon.ax[1];
const simpleAxSkill = props.gridWeapon.ax[0]
const axSkill = primaryAxSkills.find(
(skill) => skill.id == simpleAxSkill.modifier
)
return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${
axSkill?.suffix ? axSkill.suffix : ''
}`
}
return ''
}
const createSecondaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1]
if (props.gridWeapon.ax) {
const primarySimpleAxSkill = props.gridWeapon.ax[0]
const secondarySimpleAxSkill = props.gridWeapon.ax[1]
const primaryAxSkill = primaryAxSkills.find(
(skill) => skill.id == primarySimpleAxSkill.modifier
);
)
if (primaryAxSkill && primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(
(skill) => skill.id == secondarySimpleAxSkill.modifier
);
)
return `${secondaryAxSkill?.name[locale]} +${
secondarySimpleAxSkill.strength
}${secondaryAxSkill?.suffix ? secondaryAxSkill.suffix : ""}`;
}${secondaryAxSkill?.suffix ? secondaryAxSkill.suffix : ''}`
}
}
return "";
};
return ''
}
function weaponImage() {
const weapon = props.gridWeapon.object;
const weapon = props.gridWeapon.object
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`;
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
else
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`;
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
}
const keysSection = (
@ -136,10 +134,10 @@ const WeaponHovercard = (props: Props) => {
{WeaponKeyNames[props.gridWeapon.object.series] ? (
<h5 className={tintElement}>
{WeaponKeyNames[props.gridWeapon.object.series][locale]}
{locale === "en" ? "s" : ""}
{locale === 'en' ? 's' : ''}
</h5>
) : (
""
''
)}
{props.gridWeapon.weapon_keys
@ -151,21 +149,21 @@ const WeaponHovercard = (props: Props) => {
>
<span>{props.gridWeapon.weapon_keys![i].name[locale]}</span>
</div>
);
)
})
: ""}
: ''}
</section>
);
)
const axSection = (
<section className="axSkills">
<h5 className={tintElement}>{t("modals.weapon.subtitles.ax_skills")}</h5>
<h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5>
<div className="skills">
<div className="primary axSkill">
<img
alt="AX1"
src={`/icons/ax/primary_${
props.gridWeapon.ax ? props.gridWeapon.ax[0].modifier : ""
props.gridWeapon.ax ? props.gridWeapon.ax[0].modifier : ''
}.png`}
/>
<span>{createPrimaryAxSkillString()}</span>
@ -178,17 +176,17 @@ const WeaponHovercard = (props: Props) => {
<img
alt="AX2"
src={`/icons/ax/secondary_${
props.gridWeapon.ax ? props.gridWeapon.ax[1].modifier : ""
props.gridWeapon.ax ? props.gridWeapon.ax[1].modifier : ''
}.png`}
/>
<span>{createSecondaryAxSkillString()}</span>
</div>
) : (
""
''
)}
</div>
</section>
);
)
return (
<HoverCard.Root>
@ -216,7 +214,7 @@ const WeaponHovercard = (props: Props) => {
}
/>
) : (
""
''
)}
<WeaponLabelIcon
labelType={Proficiency[props.gridWeapon.object.proficiency]}
@ -236,17 +234,17 @@ const WeaponHovercard = (props: Props) => {
props.gridWeapon.ax[0].modifier &&
props.gridWeapon.ax[0].strength
? axSection
: ""}
: ''}
{props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0
? keysSection
: ""}
: ''}
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t("buttons.wiki")}
{t('buttons.wiki')}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
);
};
)
}
export default WeaponHovercard;
export default WeaponHovercard

View file

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

View file

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

View file

@ -1,18 +1,16 @@
import React from "react";
import { useRouter } from "next/router";
import React from 'react'
import { useRouter } from 'next/router'
import "./index.scss";
import './index.scss'
interface Props {
labelType: string;
labelType: string
}
const WeaponLabelIcon = (props: Props) => {
const router = useRouter();
const router = useRouter()
return (
<i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
);
};
return <i className={`WeaponLabelIcon ${props.labelType} ${router.locale}`} />
}
export default WeaponLabelIcon;
export default WeaponLabelIcon

View file

@ -1,71 +1,69 @@
import React, { useState } from "react";
import { getCookie } from "cookies-next";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { AxiosResponse } from "axios";
import React, { useState } from 'react'
import { getCookie } from 'cookies-next'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
import * as Dialog from "@radix-ui/react-dialog";
import * as Dialog from '@radix-ui/react-dialog'
import AXSelect from "~components/AxSelect";
import ElementToggle from "~components/ElementToggle";
import WeaponKeyDropdown from "~components/WeaponKeyDropdown";
import Button from "~components/Button";
import AXSelect from '~components/AxSelect'
import ElementToggle from '~components/ElementToggle'
import WeaponKeyDropdown from '~components/WeaponKeyDropdown'
import Button from '~components/Button'
import api from "~utils/api";
import { appState } from "~utils/appState";
import api from '~utils/api'
import { appState } from '~utils/appState'
import CrossIcon from "~public/icons/Cross.svg";
import "./index.scss";
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
interface GridWeaponObject {
weapon: {
element?: number;
weapon_key1_id?: string;
weapon_key2_id?: string;
weapon_key3_id?: string;
ax_modifier1?: number;
ax_modifier2?: number;
ax_strength1?: number;
ax_strength2?: number;
};
element?: number
weapon_key1_id?: string
weapon_key2_id?: string
weapon_key3_id?: string
ax_modifier1?: number
ax_modifier2?: number
ax_strength1?: number
ax_strength2?: number
}
}
interface Props {
gridWeapon: GridWeapon;
children: React.ReactNode;
gridWeapon: GridWeapon
children: React.ReactNode
}
const WeaponModal = (props: Props) => {
const router = useRouter();
const router = useRouter()
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const { t } = useTranslation("common");
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {};
: {}
// Refs
const weaponKey1Select = React.createRef<HTMLSelectElement>();
const weaponKey2Select = React.createRef<HTMLSelectElement>();
const weaponKey3Select = React.createRef<HTMLSelectElement>();
const weaponKey1Select = React.createRef<HTMLSelectElement>()
const weaponKey2Select = React.createRef<HTMLSelectElement>()
const weaponKey3Select = React.createRef<HTMLSelectElement>()
// State
const [open, setOpen] = useState(false);
const [formValid, setFormValid] = useState(false);
const [open, setOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
const [element, setElement] = useState(-1);
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1);
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1);
const [primaryAxValue, setPrimaryAxValue] = useState(0.0);
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0);
const [element, setElement] = useState(-1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
function receiveAxValues(
primaryAxModifier: number,
@ -73,82 +71,82 @@ const WeaponModal = (props: Props) => {
secondaryAxModifier: number,
secondaryAxValue: number
) {
setPrimaryAxModifier(primaryAxModifier);
setSecondaryAxModifier(secondaryAxModifier);
setPrimaryAxModifier(primaryAxModifier)
setSecondaryAxModifier(secondaryAxModifier)
setPrimaryAxValue(primaryAxValue);
setSecondaryAxValue(secondaryAxValue);
setPrimaryAxValue(primaryAxValue)
setSecondaryAxValue(secondaryAxValue)
}
function receiveAxValidity(isValid: boolean) {
setFormValid(isValid);
setFormValid(isValid)
}
function receiveElementValue(element: string) {
setElement(parseInt(element));
setElement(parseInt(element))
}
function prepareObject() {
let object: GridWeaponObject = { weapon: {} };
let object: GridWeaponObject = { weapon: {} }
if (props.gridWeapon.object.element == 0) object.weapon.element = element;
if (props.gridWeapon.object.element == 0) object.weapon.element = element
if ([2, 3, 17, 24].includes(props.gridWeapon.object.series))
object.weapon.weapon_key1_id = weaponKey1Select.current?.value;
object.weapon.weapon_key1_id = weaponKey1Select.current?.value
if ([2, 3, 17].includes(props.gridWeapon.object.series))
object.weapon.weapon_key2_id = weaponKey2Select.current?.value;
object.weapon.weapon_key2_id = weaponKey2Select.current?.value
if (props.gridWeapon.object.series == 17)
object.weapon.weapon_key3_id = weaponKey3Select.current?.value;
object.weapon.weapon_key3_id = weaponKey3Select.current?.value
if (props.gridWeapon.object.ax > 0) {
object.weapon.ax_modifier1 = primaryAxModifier;
object.weapon.ax_modifier2 = secondaryAxModifier;
object.weapon.ax_strength1 = primaryAxValue;
object.weapon.ax_strength2 = secondaryAxValue;
object.weapon.ax_modifier1 = primaryAxModifier
object.weapon.ax_modifier2 = secondaryAxModifier
object.weapon.ax_strength1 = primaryAxValue
object.weapon.ax_strength2 = secondaryAxValue
}
return object;
return object
}
async function updateWeapon() {
const updateObject = prepareObject();
const updateObject = prepareObject()
return await api.endpoints.grid_weapons
.update(props.gridWeapon.id, updateObject, headers)
.then((response) => processResult(response))
.catch((error) => processError(error));
.catch((error) => processError(error))
}
function processResult(response: AxiosResponse) {
const gridWeapon: GridWeapon = response.data.grid_weapon;
const gridWeapon: GridWeapon = response.data.grid_weapon
if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon;
else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon;
if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon
else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
setOpen(false);
setOpen(false)
}
function processError(error: any) {
console.error(error);
console.error(error)
}
const elementSelect = () => {
return (
<section>
<h3>{t("modals.weapon.subtitles.element")}</h3>
<h3>{t('modals.weapon.subtitles.element')}</h3>
<ElementToggle
currentElement={props.gridWeapon.element}
sendValue={receiveElementValue}
/>
</section>
);
};
)
}
const keySelect = () => {
return (
<section>
<h3>{t("modals.weapon.subtitles.weapon_keys")}</h3>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? (
<WeaponKeyDropdown
currentValue={
@ -161,7 +159,7 @@ const WeaponModal = (props: Props) => {
ref={weaponKey1Select}
/>
) : (
""
''
)}
{[2, 3, 17].includes(props.gridWeapon.object.series) ? (
@ -176,7 +174,7 @@ const WeaponModal = (props: Props) => {
ref={weaponKey2Select}
/>
) : (
""
''
)}
{props.gridWeapon.object.series == 17 ? (
@ -191,16 +189,16 @@ const WeaponModal = (props: Props) => {
ref={weaponKey3Select}
/>
) : (
""
''
)}
</section>
);
};
)
}
const axSelect = () => {
return (
<section>
<h3>{t("modals.weapon.subtitles.ax_skills")}</h3>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect
axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax}
@ -208,12 +206,12 @@ const WeaponModal = (props: Props) => {
sendValues={receiveAxValues}
/>
</section>
);
};
)
}
function openChange(open: boolean) {
setFormValid(false);
setOpen(open);
setFormValid(false)
setOpen(open)
}
return (
@ -227,7 +225,7 @@ const WeaponModal = (props: Props) => {
<div className="DialogHeader">
<div className="DialogTop">
<Dialog.Title className="SubTitle">
{t("modals.weapon.title")}
{t('modals.weapon.title')}
</Dialog.Title>
<Dialog.Title className="DialogTitle">
{props.gridWeapon.object.name[locale]}
@ -241,23 +239,23 @@ const WeaponModal = (props: Props) => {
</div>
<div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ""}
{props.gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series)
? keySelect()
: ""}
{props.gridWeapon.object.ax > 0 ? axSelect() : ""}
: ''}
{props.gridWeapon.object.ax > 0 ? axSelect() : ''}
<Button
onClick={updateWeapon}
disabled={props.gridWeapon.object.ax > 0 && !formValid}
>
{t("modals.weapon.buttons.confirm")}
{t('modals.weapon.buttons.confirm')}
</Button>
</div>
</Dialog.Content>
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
);
};
)
}
export default WeaponModal;
export default WeaponModal

View file

@ -1,65 +1,63 @@
import React from "react";
import { useRouter } from "next/router";
import React from 'react'
import { useRouter } from 'next/router'
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import "./index.scss";
import './index.scss'
interface Props {
data: Weapon;
onClick: () => void;
data: Weapon
onClick: () => void
}
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = [
"none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
'none',
'sword',
'dagger',
'axe',
'spear',
'bow',
'staff',
'fist',
'harp',
'gun',
'katana',
]
const Series = [
"seraphic",
"grand",
"opus",
"draconic",
"revenant",
"primal",
"beast",
"regalia",
"omega",
"olden_primal",
"hollowsky",
"xeno",
"astral",
"rose",
"ultima",
"bahamut",
"epic",
"ennead",
"cosmos",
"ancestral",
"superlative",
"vintage",
"class_champion",
"sephira",
"new_world_foundation",
];
'seraphic',
'grand',
'opus',
'draconic',
'revenant',
'primal',
'beast',
'regalia',
'omega',
'olden_primal',
'hollowsky',
'xeno',
'astral',
'rose',
'ultima',
'bahamut',
'epic',
'ennead',
'cosmos',
'ancestral',
'superlative',
'vintage',
'class_champion',
'sephira',
'new_world_foundation',
]
const WeaponResult = (props: Props) => {
const router = useRouter();
const router = useRouter()
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const weapon = props.data;
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const weapon = props.data
return (
<li className="WeaponResult" onClick={props.onClick}>
@ -81,7 +79,7 @@ const WeaponResult = (props: Props) => {
</div>
</div>
</li>
);
};
)
}
export default WeaponResult;
export default WeaponResult

View file

@ -1,141 +1,141 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import cloneDeep from "lodash.clonedeep";
import cloneDeep from 'lodash.clonedeep'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import SearchFilter from "~components/SearchFilter";
import SearchFilterCheckboxItem from "~components/SearchFilterCheckboxItem";
import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import "./index.scss";
import './index.scss'
import {
emptyElementState,
emptyProficiencyState,
emptyRarityState,
emptyWeaponSeriesState,
} from "~utils/emptyStates";
} from '~utils/emptyStates'
import {
elements,
proficiencies,
rarities,
weaponSeries,
} from "~utils/stateValues";
} from '~utils/stateValues'
interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void;
sendFilters: (filters: { [key: string]: number[] }) => void
}
const WeaponSearchFilterBar = (props: Props) => {
const { t } = useTranslation("common");
const { t } = useTranslation('common')
const [rarityMenu, setRarityMenu] = useState(false);
const [elementMenu, setElementMenu] = useState(false);
const [proficiencyMenu, setProficiencyMenu] = useState(false);
const [seriesMenu, setSeriesMenu] = useState(false);
const [rarityMenu, setRarityMenu] = useState(false)
const [elementMenu, setElementMenu] = useState(false)
const [proficiencyMenu, setProficiencyMenu] = useState(false)
const [seriesMenu, setSeriesMenu] = useState(false)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState);
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] =
useState<ElementState>(emptyElementState);
useState<ElementState>(emptyElementState)
const [proficiencyState, setProficiencyState] = useState<ProficiencyState>(
emptyProficiencyState
);
)
const [seriesState, setSeriesState] = useState<WeaponSeriesState>(
emptyWeaponSeriesState
);
)
function rarityMenuOpened(open: boolean) {
if (open) {
setRarityMenu(true);
setElementMenu(false);
setProficiencyMenu(false);
setSeriesMenu(false);
} else setRarityMenu(false);
setRarityMenu(true)
setElementMenu(false)
setProficiencyMenu(false)
setSeriesMenu(false)
} else setRarityMenu(false)
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(true);
setProficiencyMenu(false);
setSeriesMenu(false);
} else setElementMenu(false);
setRarityMenu(false)
setElementMenu(true)
setProficiencyMenu(false)
setSeriesMenu(false)
} else setElementMenu(false)
}
function proficiencyMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(false);
setProficiencyMenu(true);
setSeriesMenu(false);
} else setProficiencyMenu(false);
setRarityMenu(false)
setElementMenu(false)
setProficiencyMenu(true)
setSeriesMenu(false)
} else setProficiencyMenu(false)
}
function seriesMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(false);
setProficiencyMenu(false);
setSeriesMenu(true);
} else setSeriesMenu(false);
setRarityMenu(false)
setElementMenu(false)
setProficiencyMenu(false)
setSeriesMenu(true)
} else setSeriesMenu(false)
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState);
newRarityState[key].checked = checked;
setRarityState(newRarityState);
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);
let newElementState = cloneDeep(elementState)
newElementState[key].checked = checked
setElementState(newElementState)
}
function handleProficiencyChange(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiencyState);
newProficiencyState[key].checked = checked;
setProficiencyState(newProficiencyState);
let newProficiencyState = cloneDeep(proficiencyState)
newProficiencyState[key].checked = checked
setProficiencyState(newProficiencyState)
}
function handleSeriesChange(checked: boolean, key: string) {
let newSeriesState = cloneDeep(seriesState);
newSeriesState[key].checked = checked;
setSeriesState(newSeriesState);
let newSeriesState = cloneDeep(seriesState)
newSeriesState[key].checked = checked
setSeriesState(newSeriesState)
}
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const checkedProficiencyFilters = Object.values(proficiencyState)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const checkedSeriesFilters = Object.values(seriesState)
.filter((x) => x.checked)
.map((x, i) => x.id);
.map((x, i) => x.id)
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiencyFilters,
series: checkedSeriesFilters,
};
}
props.sendFilters(filters);
props.sendFilters(filters)
}
useEffect(() => {
sendFilters();
}, [rarityState, elementState, proficiencyState, seriesState]);
sendFilters()
}, [rarityState, elementState, proficiencyState, seriesState])
return (
<div className="SearchFilterBar">
<SearchFilter
label={t("filters.labels.rarity")}
label={t('filters.labels.rarity')}
numSelected={
Object.values(rarityState)
.map((x) => x.checked)
@ -145,7 +145,7 @@ const WeaponSearchFilterBar = (props: Props) => {
onOpenChange={rarityMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.rarity")}
{t('filters.labels.rarity')}
</DropdownMenu.Label>
{Array.from(Array(rarities.length)).map((x, i) => {
return (
@ -157,12 +157,12 @@ const WeaponSearchFilterBar = (props: Props) => {
>
{t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</SearchFilter>
<SearchFilter
label={t("filters.labels.element")}
label={t('filters.labels.element')}
numSelected={
Object.values(elementState)
.map((x) => x.checked)
@ -172,7 +172,7 @@ const WeaponSearchFilterBar = (props: Props) => {
onOpenChange={elementMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.element")}
{t('filters.labels.element')}
</DropdownMenu.Label>
{Array.from(Array(elements.length)).map((x, i) => {
return (
@ -184,12 +184,12 @@ const WeaponSearchFilterBar = (props: Props) => {
>
{t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</SearchFilter>
<SearchFilter
label={t("filters.labels.proficiency")}
label={t('filters.labels.proficiency')}
numSelected={
Object.values(proficiencyState)
.map((x) => x.checked)
@ -199,7 +199,7 @@ const WeaponSearchFilterBar = (props: Props) => {
onOpenChange={proficiencyMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.proficiency")}
{t('filters.labels.proficiency')}
</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
@ -213,7 +213,7 @@ const WeaponSearchFilterBar = (props: Props) => {
>
{t(`proficiencies.${proficiencies[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
@ -235,14 +235,14 @@ const WeaponSearchFilterBar = (props: Props) => {
}`
)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
</section>
</SearchFilter>
<SearchFilter
label={t("filters.labels.series")}
label={t('filters.labels.series')}
numSelected={
Object.values(seriesState)
.map((x) => x.checked)
@ -252,7 +252,7 @@ const WeaponSearchFilterBar = (props: Props) => {
onOpenChange={seriesMenuOpened}
>
<DropdownMenu.Label className="Label">
{t("filters.labels.series")}
{t('filters.labels.series')}
</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
@ -266,7 +266,7 @@ const WeaponSearchFilterBar = (props: Props) => {
>
{t(`series.${weaponSeries[i]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
@ -283,7 +283,7 @@ const WeaponSearchFilterBar = (props: Props) => {
>
{t(`series.${weaponSeries[i + weaponSeries.length / 3]}`)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
@ -302,13 +302,13 @@ const WeaponSearchFilterBar = (props: Props) => {
`series.${weaponSeries[i + 2 * (weaponSeries.length / 3)]}`
)}
</SearchFilterCheckboxItem>
);
)
})}
</DropdownMenu.Group>
</section>
</SearchFilter>
</div>
);
};
)
}
export default WeaponSearchFilterBar;
export default WeaponSearchFilterBar

View file

@ -1,39 +1,37 @@
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 WeaponModal from "~components/WeaponModal";
import WeaponHovercard from "~components/WeaponHovercard";
import UncapIndicator from "~components/UncapIndicator";
import Button from "~components/Button";
import SearchModal from '~components/SearchModal'
import WeaponModal from '~components/WeaponModal'
import WeaponHovercard from '~components/WeaponHovercard'
import UncapIndicator from '~components/UncapIndicator'
import Button from '~components/Button'
import { ButtonType } from "~utils/enums";
import type { SearchableObject } from "~types";
import { ButtonType } from '~utils/enums'
import type { SearchableObject } from '~types'
import PlusIcon from "~public/icons/Add.svg";
import "./index.scss";
import PlusIcon from '~public/icons/Add.svg'
import './index.scss'
interface Props {
gridWeapon: GridWeapon | undefined;
unitType: 0 | 1;
position: number;
editable: boolean;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
gridWeapon: GridWeapon | undefined
unitType: 0 | 1
position: number
editable: boolean
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const WeaponUnit = (props: Props) => {
const { t } = useTranslation("common");
const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState("");
const [imageUrl, setImageUrl] = useState('')
const router = useRouter();
const router = useRouter()
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const classes = classnames({
WeaponUnit: true,
@ -41,48 +39,48 @@ const WeaponUnit = (props: Props) => {
grid: props.unitType == 1,
editable: props.editable,
filled: props.gridWeapon !== undefined,
});
})
const gridWeapon = props.gridWeapon;
const weapon = gridWeapon?.object;
const gridWeapon = props.gridWeapon
const weapon = gridWeapon?.object
useEffect(() => {
generateImageUrl();
});
generateImageUrl()
})
function generateImageUrl() {
let imgSrc = "";
let imgSrc = ''
if (props.gridWeapon) {
const weapon = props.gridWeapon.object!;
const weapon = props.gridWeapon.object!
if (props.unitType == 0) {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
} else {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`;
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
}
}
setImageUrl(imgSrc);
setImageUrl(imgSrc)
}
function passUncapData(uncap: number) {
if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, props.position, uncap);
props.updateUncap(props.gridWeapon.id, props.position, uncap)
}
function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object;
const weapon = gridWeapon.object
return (
weapon.ax > 0 ||
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
);
)
}
const image = (
@ -93,21 +91,21 @@ const WeaponUnit = (props: Props) => {
<PlusIcon />
</span>
) : (
""
''
)}
</div>
);
)
const editableImage = (
<SearchModal
placeholderText={t("search.placeholders.weapon")}
placeholderText={t('search.placeholders.weapon')}
fromPosition={props.position}
object="weapons"
send={props.updateObject}
>
{image}
</SearchModal>
);
)
const unitContent = (
<div className={classes}>
@ -118,7 +116,7 @@ const WeaponUnit = (props: Props) => {
</div>
</WeaponModal>
) : (
""
''
)}
{props.editable ? editableImage : image}
{gridWeapon ? (
@ -131,17 +129,17 @@ const WeaponUnit = (props: Props) => {
special={false}
/>
) : (
""
''
)}
<h3 className="WeaponName">{weapon?.name[locale]}</h3>
</div>
);
)
const withHovercard = (
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard>
);
)
return gridWeapon && !props.editable ? withHovercard : unitContent;
};
return gridWeapon && !props.editable ? withHovercard : unitContent
}
export default WeaponUnit;
export default WeaponUnit

View file

@ -1,121 +1,121 @@
import React, { useCallback, useEffect, useState } from "react";
import Head from "next/head";
import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head'
import { getCookie } from "cookies-next";
import { queryTypes, useQueryState } from "next-usequerystate";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import InfiniteScroll from "react-infinite-scroll-component";
import { getCookie } from 'cookies-next'
import { queryTypes, useQueryState } from 'next-usequerystate'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import InfiniteScroll from 'react-infinite-scroll-component'
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import api from "~utils/api";
import useDidMountEffect from "~utils/useDidMountEffect";
import { elements, allElement } from "~utils/Element";
import api from '~utils/api'
import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element'
import GridRep from "~components/GridRep";
import GridRepCollection from "~components/GridRepCollection";
import FilterBar from "~components/FilterBar";
import GridRep from '~components/GridRep'
import GridRepCollection from '~components/GridRepCollection'
import FilterBar from '~components/FilterBar'
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextApiRequest, NextApiResponse } from 'next'
interface Props {
user?: User;
teams?: { count: number; total_pages: number; results: Party[] };
raids: Raid[];
sortedRaids: Raid[][];
user?: User
teams?: { count: number; total_pages: number; results: Party[] }
raids: Raid[]
sortedRaids: Raid[][]
}
const ProfileRoute: React.FC<Props> = (props: Props) => {
// Set up cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {};
: {}
// Set up router
const router = useRouter();
const { username } = router.query;
const router = useRouter()
const { username } = router.query
// Import translations
const { t } = useTranslation("common");
const { t } = useTranslation('common')
// Set up app-specific states
const [raidsLoading, setRaidsLoading] = useState(true);
const [scrolled, setScrolled] = useState(false);
const [raidsLoading, setRaidsLoading] = useState(true)
const [scrolled, setScrolled] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([]);
const [raids, setRaids] = useState<Raid[]>();
const [raid, setRaid] = useState<Raid>();
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>()
const [raid, setRaid] = useState<Raid>()
// Set up infinite scrolling-related states
const [recordCount, setRecordCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [recordCount, setRecordCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
// Set up filter-specific query states
// Recency is in seconds
const [element, setElement] = useQueryState("element", {
const [element, setElement] = useQueryState('element', {
defaultValue: -1,
parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value),
});
const [raidSlug, setRaidSlug] = useQueryState("raid", {
defaultValue: "all",
});
})
const [raidSlug, setRaidSlug] = useQueryState('raid', {
defaultValue: 'all',
})
const [recency, setRecency] = useQueryState(
"recency",
'recency',
queryTypes.integer.withDefault(-1)
);
)
// Define transformers for element
function parseElement(query: string) {
let element: TeamElement | undefined =
query === "all"
query === 'all'
? allElement
: elements.find((element) => element.name.en.toLowerCase() === query);
return element ? element.id : -1;
: elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
}
function serializeElement(value: number | undefined) {
let name = "";
let name = ''
if (value != undefined) {
if (value == -1) name = allElement.name.en.toLowerCase();
else name = elements[value].name.en.toLowerCase();
if (value == -1) name = allElement.name.en.toLowerCase()
else name = elements[value].name.en.toLowerCase()
}
return name;
return name
}
// Set the initial parties from props
useEffect(() => {
if (props.teams) {
setTotalPages(props.teams.total_pages);
setRecordCount(props.teams.count);
replaceResults(props.teams.count, props.teams.results);
setTotalPages(props.teams.total_pages)
setRecordCount(props.teams.count)
replaceResults(props.teams.count, props.teams.results)
}
setCurrentPage(1);
}, []);
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Handle errors
const handleError = useCallback((error: any) => {
if (error.response != null) {
console.error(error);
console.error(error)
} else {
console.error("There was an error.");
console.error('There was an error.')
}
}, []);
}, [])
const fetchProfile = useCallback(
({ replace }: { replace: boolean }) => {
@ -126,7 +126,7 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
recency: recency != -1 ? recency : undefined,
page: currentPage,
},
};
}
if (username && !Array.isArray(username)) {
api.endpoints.users
@ -135,62 +135,62 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
params: { ...filters, ...{ headers: headers } },
})
.then((response) => {
setTotalPages(response.data.parties.total_pages);
setRecordCount(response.data.parties.count);
setTotalPages(response.data.parties.total_pages)
setRecordCount(response.data.parties.count)
if (replace)
replaceResults(
response.data.parties.count,
response.data.parties.results
);
else appendResults(response.data.parties.results);
)
else appendResults(response.data.parties.results)
})
.catch((error) => handleError(error));
.catch((error) => handleError(error))
}
},
[currentPage, parties, element, raid, recency]
);
)
function replaceResults(count: number, list: Party[]) {
if (count > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)));
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else {
setParties([]);
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list]);
setParties([...parties, ...list])
}
// Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => {
api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid);
setRaids(cleanRaids);
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
setRaids(cleanRaids)
setRaidsLoading(false);
setRaidsLoading(false)
const raid = cleanRaids.find((r) => r.slug === raidSlug);
setRaid(raid);
const raid = cleanRaids.find((r) => r.slug === raidSlug)
setRaid(raid)
return raid;
});
}, [setRaids]);
return raid
})
}, [setRaids])
// When the element, raid or recency filter changes,
// fetch all teams again.
useDidMountEffect(() => {
setCurrentPage(1);
fetchProfile({ replace: true });
}, [element, raid, recency]);
setCurrentPage(1)
fetchProfile({ replace: true })
}, [element, raid, recency])
// When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed
if (currentPage > 1) fetchProfile({ replace: false });
else if (currentPage == 1) fetchProfile({ replace: true });
}, [currentPage]);
if (currentPage > 1) fetchProfile({ replace: false })
else if (currentPage == 1) fetchProfile({ replace: true })
}, [currentPage])
// Receive filters from the filter bar
function receiveFilters({
@ -198,30 +198,30 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
raidSlug,
recency,
}: {
element?: number;
raidSlug?: string;
recency?: number;
element?: number
raidSlug?: string
recency?: number
}) {
if (element == 0) setElement(0);
else if (element) setElement(element);
if (element == 0) setElement(0)
else if (element) setElement(element)
if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug);
setRaid(raid);
setRaidSlug(raidSlug);
const raid = raids.find((raid) => raid.slug === raidSlug)
setRaid(raid)
setRaidSlug(raidSlug)
}
if (recency) setRecency(recency);
if (recency) setRecency(recency)
}
// Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true);
else setScrolled(false);
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) {
router.push(`/p/${shortcode}`);
router.push(`/p/${shortcode}`)
}
// TODO: Add save functions
@ -240,8 +240,8 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
key={`party-${i}`}
onClick={goTo}
/>
);
});
)
})
}
return (
@ -309,25 +309,25 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
{parties.length == 0 ? (
<div id="NotFound">
<h2>{t("teams.not_found")}</h2>
<h2>{t('teams.not_found')}</h2>
</div>
) : (
""
''
)}
</section>
</div>
);
};
)
}
export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: "string" } },
{ params: { party: 'string' } },
],
fallback: true,
};
};
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -403,31 +403,31 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
const organizeRaids = (raids: Raid[]) => {
// Set up empty raid for "All raids"
const all = {
id: "0",
id: '0',
name: {
en: "All raids",
ja: "全て",
en: 'All raids',
ja: '全て',
},
slug: "all",
slug: 'all',
level: 0,
group: 0,
element: 0,
};
}
const numGroups = Math.max.apply(
Math,
raids.map((raid) => raid.group)
);
let groupedRaids = [];
)
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)
}
return {
raids: raids,
sortedRaids: groupedRaids,
};
};
}
}
export default ProfileRoute;
export default ProfileRoute

View file

@ -1,17 +1,17 @@
import { useEffect } from "react"
import { getCookie } from "cookies-next"
import { appWithTranslation } from "next-i18next"
import { ThemeProvider } from "next-themes"
import { useEffect } from 'react'
import { getCookie } from 'cookies-next'
import { appWithTranslation } from 'next-i18next'
import { ThemeProvider } from 'next-themes'
import type { AppProps } from "next/app"
import Layout from "~components/Layout"
import type { AppProps } from 'next/app'
import Layout from '~components/Layout'
import { accountState } from "~utils/accountState"
import { accountState } from '~utils/accountState'
import "../styles/globals.scss"
import '../styles/globals.scss'
function MyApp({ Component, pageProps }: AppProps) {
const cookie = getCookie("account")
const cookie = getCookie('account')
const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null
useEffect(() => {
@ -22,8 +22,8 @@ function MyApp({ Component, pageProps }: AppProps) {
accountState.account.user = {
id: cookieData.userId,
username: cookieData.username,
picture: "",
element: "",
picture: '',
element: '',
gender: 0,
}
} else {

View file

@ -1,53 +1,53 @@
import React, { useEffect } from "react";
import { getCookie } from "cookies-next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import React, { useEffect } from 'react'
import { getCookie } from 'cookies-next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from "~components/Party";
import Party from '~components/Party'
import { appState } from "~utils/appState";
import api from "~utils/api";
import { appState } from '~utils/appState'
import api from '~utils/api'
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextApiRequest, NextApiResponse } from 'next'
interface Props {
jobs: Job[];
jobSkills: JobSkill[];
raids: Raid[];
sortedRaids: Raid[][];
jobs: Job[]
jobSkills: JobSkill[]
raids: Raid[]
sortedRaids: Raid[][]
}
const NewRoute: React.FC<Props> = (props: Props) => {
function callback(path: string) {
// This is scuffed, how do we do this natively?
window.history.replaceState(null, `Grid Tool`, `${path}`);
window.history.replaceState(null, `Grid Tool`, `${path}`)
}
useEffect(() => {
persistStaticData();
}, [persistStaticData]);
persistStaticData()
}, [persistStaticData])
function persistStaticData() {
appState.raids = props.raids;
appState.jobs = props.jobs;
appState.jobSkills = props.jobSkills;
appState.raids = props.raids
appState.jobs = props.jobs
appState.jobSkills = props.jobSkills
}
return (
<div id="Content">
<Party new={true} raids={props.sortedRaids} pushHistory={callback} />
</div>
);
};
)
}
export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: "string" } },
{ params: { party: 'string' } },
],
fallback: true,
};
};
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -87,31 +87,31 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
const organizeRaids = (raids: Raid[]) => {
// Set up empty raid for "All raids"
const all = {
id: "0",
id: '0',
name: {
en: "All raids",
ja: "全て",
en: 'All raids',
ja: '全て',
},
slug: "all",
slug: 'all',
level: 0,
group: 0,
element: 0,
};
}
const numGroups = Math.max.apply(
Math,
raids.map((raid) => raid.group)
);
let groupedRaids = [];
)
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)
}
return {
raids: raids,
sortedRaids: groupedRaids,
};
};
}
}
export default NewRoute;
export default NewRoute

View file

@ -1,120 +1,120 @@
import React, { useCallback, useEffect, useState } from "react";
import Head from "next/head";
import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head'
import { getCookie } from "cookies-next";
import { queryTypes, useQueryState } from "next-usequerystate";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import InfiniteScroll from "react-infinite-scroll-component";
import { getCookie } from 'cookies-next'
import { queryTypes, useQueryState } from 'next-usequerystate'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import InfiniteScroll from 'react-infinite-scroll-component'
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import clonedeep from "lodash.clonedeep";
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep'
import api from "~utils/api";
import useDidMountEffect from "~utils/useDidMountEffect";
import { elements, allElement } from "~utils/Element";
import api from '~utils/api'
import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element'
import GridRep from "~components/GridRep";
import GridRepCollection from "~components/GridRepCollection";
import FilterBar from "~components/FilterBar";
import GridRep from '~components/GridRep'
import GridRepCollection from '~components/GridRepCollection'
import FilterBar from '~components/FilterBar'
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextApiRequest, NextApiResponse } from 'next'
interface Props {
teams?: { count: number; total_pages: number; results: Party[] };
raids: Raid[];
sortedRaids: Raid[][];
teams?: { count: number; total_pages: number; results: Party[] }
raids: Raid[]
sortedRaids: Raid[][]
}
const SavedRoute: React.FC<Props> = (props: Props) => {
// Set up cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {};
: {}
// Set up router
const router = useRouter();
const router = useRouter()
// Import translations
const { t } = useTranslation("common");
const { t } = useTranslation('common')
// Set up app-specific states
const [raidsLoading, setRaidsLoading] = useState(true);
const [scrolled, setScrolled] = useState(false);
const [raidsLoading, setRaidsLoading] = useState(true)
const [scrolled, setScrolled] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([]);
const [raids, setRaids] = useState<Raid[]>();
const [raid, setRaid] = useState<Raid>();
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>()
const [raid, setRaid] = useState<Raid>()
// Set up infinite scrolling-related states
const [recordCount, setRecordCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [recordCount, setRecordCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
// Set up filter-specific query states
// Recency is in seconds
const [element, setElement] = useQueryState("element", {
const [element, setElement] = useQueryState('element', {
defaultValue: -1,
parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value),
});
const [raidSlug, setRaidSlug] = useQueryState("raid", {
defaultValue: "all",
});
})
const [raidSlug, setRaidSlug] = useQueryState('raid', {
defaultValue: 'all',
})
const [recency, setRecency] = useQueryState(
"recency",
'recency',
queryTypes.integer.withDefault(-1)
);
)
// Define transformers for element
function parseElement(query: string) {
let element: TeamElement | undefined =
query === "all"
query === 'all'
? allElement
: elements.find((element) => element.name.en.toLowerCase() === query);
return element ? element.id : -1;
: elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
}
function serializeElement(value: number | undefined) {
let name = "";
let name = ''
if (value != undefined) {
if (value == -1) name = allElement.name.en.toLowerCase();
else name = elements[value].name.en.toLowerCase();
if (value == -1) name = allElement.name.en.toLowerCase()
else name = elements[value].name.en.toLowerCase()
}
return name;
return name
}
// Set the initial parties from props
useEffect(() => {
if (props.teams) {
setTotalPages(props.teams.total_pages);
setRecordCount(props.teams.count);
replaceResults(props.teams.count, props.teams.results);
setTotalPages(props.teams.total_pages)
setRecordCount(props.teams.count)
replaceResults(props.teams.count, props.teams.results)
}
setCurrentPage(1);
}, []);
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Handle errors
const handleError = useCallback((error: any) => {
if (error.response != null) {
console.error(error);
console.error(error)
} else {
console.error("There was an error.");
console.error('There was an error.')
}
}, []);
}, [])
const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => {
@ -125,63 +125,63 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
recency: recency != -1 ? recency : undefined,
page: currentPage,
},
};
}
api
.savedTeams({ ...filters, ...{ headers: headers } })
.then((response) => {
setTotalPages(response.data.total_pages);
setRecordCount(response.data.count);
setTotalPages(response.data.total_pages)
setRecordCount(response.data.count)
if (replace)
replaceResults(response.data.count, response.data.results);
else appendResults(response.data.results);
replaceResults(response.data.count, response.data.results)
else appendResults(response.data.results)
})
.catch((error) => handleError(error));
.catch((error) => handleError(error))
},
[currentPage, parties, element, raid, recency]
);
)
function replaceResults(count: number, list: Party[]) {
if (count > 0) {
setParties(list);
setParties(list)
} else {
setParties([]);
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list]);
setParties([...parties, ...list])
}
// Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => {
api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid);
setRaids(cleanRaids);
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
setRaids(cleanRaids)
setRaidsLoading(false);
setRaidsLoading(false)
const raid = cleanRaids.find((r) => r.slug === raidSlug);
setRaid(raid);
const raid = cleanRaids.find((r) => r.slug === raidSlug)
setRaid(raid)
return raid;
});
}, [setRaids]);
return raid
})
}, [setRaids])
// When the element, raid or recency filter changes,
// fetch all teams again.
useDidMountEffect(() => {
setCurrentPage(1);
fetchTeams({ replace: true });
}, [element, raid, recency]);
setCurrentPage(1)
fetchTeams({ replace: true })
}, [element, raid, recency])
// When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed
if (currentPage > 1) fetchTeams({ replace: false });
else if (currentPage == 1) fetchTeams({ replace: true });
}, [currentPage]);
if (currentPage > 1) fetchTeams({ replace: false })
else if (currentPage == 1) fetchTeams({ replace: true })
}, [currentPage])
// Receive filters from the filter bar
function receiveFilters({
@ -189,68 +189,68 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
raidSlug,
recency,
}: {
element?: number;
raidSlug?: string;
recency?: number;
element?: number
raidSlug?: string
recency?: number
}) {
if (element == 0) setElement(0);
else if (element) setElement(element);
if (element == 0) setElement(0)
else if (element) setElement(element)
if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug);
setRaid(raid);
setRaidSlug(raidSlug);
const raid = raids.find((raid) => raid.slug === raidSlug)
setRaid(raid)
setRaidSlug(raidSlug)
}
if (recency) setRecency(recency);
if (recency) setRecency(recency)
}
// Methods: Favorites
function toggleFavorite(teamId: string, favorited: boolean) {
if (favorited) unsaveFavorite(teamId);
else saveFavorite(teamId);
if (favorited) unsaveFavorite(teamId)
else saveFavorite(teamId)
}
function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId, params: headers }).then((response) => {
if (response.status == 201) {
const index = parties.findIndex((p) => p.id === teamId);
const party = parties[index];
const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index]
party.favorited = true;
party.favorited = true
let clonedParties = clonedeep(parties);
clonedParties[index] = party;
let clonedParties = clonedeep(parties)
clonedParties[index] = party
setParties(clonedParties);
setParties(clonedParties)
}
});
})
}
function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId, params: headers }).then((response) => {
if (response.status == 200) {
const index = parties.findIndex((p) => p.id === teamId);
const party = parties[index];
const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index]
party.favorited = false;
party.favorited = false
let clonedParties = clonedeep(parties);
clonedParties.splice(index, 1);
let clonedParties = clonedeep(parties)
clonedParties.splice(index, 1)
setParties(clonedParties);
setParties(clonedParties)
}
});
})
}
// Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true);
else setScrolled(false);
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) {
router.push(`/p/${shortcode}`);
router.push(`/p/${shortcode}`)
}
function renderParties() {
@ -270,14 +270,14 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
onClick={goTo}
onSave={toggleFavorite}
/>
);
});
)
})
}
return (
<div id="Teams">
<Head>
<title>{t("saved.title")}</title>
<title>{t('saved.title')}</title>
<meta property="og:title" content="Your saved Teams" />
<meta property="og:url" content="https://app.granblue.team/saved" />
@ -295,7 +295,7 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
raidSlug={raidSlug ? raidSlug : undefined}
recency={recency}
>
<h1>{t("saved.title")}</h1>
<h1>{t('saved.title')}</h1>
</FilterBar>
<section>
@ -314,25 +314,25 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
{parties.length == 0 ? (
<div id="NotFound">
<h2>{t("saved.not_found")}</h2>
<h2>{t('saved.not_found')}</h2>
</div>
) : (
""
''
)}
</section>
</div>
);
};
)
}
export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: "string" } },
{ params: { party: 'string' } },
],
fallback: true,
};
};
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -399,31 +399,31 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
const organizeRaids = (raids: Raid[]) => {
// Set up empty raid for "All raids"
const all = {
id: "0",
id: '0',
name: {
en: "All raids",
ja: "全て",
en: 'All raids',
ja: '全て',
},
slug: "all",
slug: 'all',
level: 0,
group: 0,
element: 0,
};
}
const numGroups = Math.max.apply(
Math,
raids.map((raid) => raid.group)
);
let groupedRaids = [];
)
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)
}
return {
raids: raids,
sortedRaids: groupedRaids,
};
};
}
}
export default SavedRoute;
export default SavedRoute

View file

@ -1,120 +1,120 @@
import React, { useCallback, useEffect, useState } from "react";
import Head from "next/head";
import React, { useCallback, useEffect, useState } from 'react'
import Head from 'next/head'
import { getCookie } from "cookies-next";
import { queryTypes, useQueryState } from "next-usequerystate";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import InfiniteScroll from "react-infinite-scroll-component";
import { getCookie } from 'cookies-next'
import { queryTypes, useQueryState } from 'next-usequerystate'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import InfiniteScroll from 'react-infinite-scroll-component'
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import clonedeep from "lodash.clonedeep";
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep'
import api from "~utils/api";
import useDidMountEffect from "~utils/useDidMountEffect";
import { elements, allElement } from "~utils/Element";
import api from '~utils/api'
import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element'
import GridRep from "~components/GridRep";
import GridRepCollection from "~components/GridRepCollection";
import FilterBar from "~components/FilterBar";
import GridRep from '~components/GridRep'
import GridRepCollection from '~components/GridRepCollection'
import FilterBar from '~components/FilterBar'
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextApiRequest, NextApiResponse } from 'next'
interface Props {
teams?: { count: number; total_pages: number; results: Party[] };
raids: Raid[];
sortedRaids: Raid[][];
teams?: { count: number; total_pages: number; results: Party[] }
raids: Raid[]
sortedRaids: Raid[][]
}
const TeamsRoute: React.FC<Props> = (props: Props) => {
// Set up cookies
const cookie = getCookie("account");
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {};
: {}
// Set up router
const router = useRouter();
const router = useRouter()
// Import translations
const { t } = useTranslation("common");
const { t } = useTranslation('common')
// Set up app-specific states
const [raidsLoading, setRaidsLoading] = useState(true);
const [scrolled, setScrolled] = useState(false);
const [raidsLoading, setRaidsLoading] = useState(true)
const [scrolled, setScrolled] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([]);
const [raids, setRaids] = useState<Raid[]>();
const [raid, setRaid] = useState<Raid>();
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>()
const [raid, setRaid] = useState<Raid>()
// Set up infinite scrolling-related states
const [recordCount, setRecordCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [recordCount, setRecordCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
// Set up filter-specific query states
// Recency is in seconds
const [element, setElement] = useQueryState("element", {
const [element, setElement] = useQueryState('element', {
defaultValue: -1,
parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value),
});
const [raidSlug, setRaidSlug] = useQueryState("raid", {
defaultValue: "all",
});
})
const [raidSlug, setRaidSlug] = useQueryState('raid', {
defaultValue: 'all',
})
const [recency, setRecency] = useQueryState(
"recency",
'recency',
queryTypes.integer.withDefault(-1)
);
)
// Define transformers for element
function parseElement(query: string) {
let element: TeamElement | undefined =
query === "all"
query === 'all'
? allElement
: elements.find((element) => element.name.en.toLowerCase() === query);
return element ? element.id : -1;
: elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
}
function serializeElement(value: number | undefined) {
let name = "";
let name = ''
if (value != undefined) {
if (value == -1) name = allElement.name.en.toLowerCase();
else name = elements[value].name.en.toLowerCase();
if (value == -1) name = allElement.name.en.toLowerCase()
else name = elements[value].name.en.toLowerCase()
}
return name;
return name
}
// Set the initial parties from props
useEffect(() => {
if (props.teams) {
setTotalPages(props.teams.total_pages);
setRecordCount(props.teams.count);
replaceResults(props.teams.count, props.teams.results);
setTotalPages(props.teams.total_pages)
setRecordCount(props.teams.count)
replaceResults(props.teams.count, props.teams.results)
}
setCurrentPage(1);
}, []);
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Handle errors
const handleError = useCallback((error: any) => {
if (error.response != null) {
console.error(error);
console.error(error)
} else {
console.error("There was an error.");
console.error('There was an error.')
}
}, []);
}, [])
const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => {
@ -125,63 +125,63 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
recency: recency != -1 ? recency : undefined,
page: currentPage,
},
};
}
api.endpoints.parties
.getAll({ ...filters, ...{ headers: headers } })
.then((response) => {
setTotalPages(response.data.total_pages);
setRecordCount(response.data.count);
setTotalPages(response.data.total_pages)
setRecordCount(response.data.count)
if (replace)
replaceResults(response.data.count, response.data.results);
else appendResults(response.data.results);
replaceResults(response.data.count, response.data.results)
else appendResults(response.data.results)
})
.catch((error) => handleError(error));
.catch((error) => handleError(error))
},
[currentPage, parties, element, raid, recency]
);
)
function replaceResults(count: number, list: Party[]) {
if (count > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)));
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else {
setParties([]);
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list]);
setParties([...parties, ...list])
}
// Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => {
api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid);
setRaids(cleanRaids);
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
setRaids(cleanRaids)
setRaidsLoading(false);
setRaidsLoading(false)
const raid = cleanRaids.find((r) => r.slug === raidSlug);
setRaid(raid);
const raid = cleanRaids.find((r) => r.slug === raidSlug)
setRaid(raid)
return raid;
});
}, [setRaids]);
return raid
})
}, [setRaids])
// When the element, raid or recency filter changes,
// fetch all teams again.
useDidMountEffect(() => {
setCurrentPage(1);
fetchTeams({ replace: true });
}, [element, raid, recency]);
setCurrentPage(1)
fetchTeams({ replace: true })
}, [element, raid, recency])
// When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed
if (currentPage > 1) fetchTeams({ replace: false });
else if (currentPage == 1) fetchTeams({ replace: true });
}, [currentPage]);
if (currentPage > 1) fetchTeams({ replace: false })
else if (currentPage == 1) fetchTeams({ replace: true })
}, [currentPage])
// Receive filters from the filter bar
function receiveFilters({
@ -189,68 +189,68 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
raidSlug,
recency,
}: {
element?: number;
raidSlug?: string;
recency?: number;
element?: number
raidSlug?: string
recency?: number
}) {
if (element == 0) setElement(0);
else if (element) setElement(element);
if (element == 0) setElement(0)
else if (element) setElement(element)
if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug);
setRaid(raid);
setRaidSlug(raidSlug);
const raid = raids.find((raid) => raid.slug === raidSlug)
setRaid(raid)
setRaidSlug(raidSlug)
}
if (recency) setRecency(recency);
if (recency) setRecency(recency)
}
// Methods: Favorites
function toggleFavorite(teamId: string, favorited: boolean) {
if (favorited) unsaveFavorite(teamId);
else saveFavorite(teamId);
if (favorited) unsaveFavorite(teamId)
else saveFavorite(teamId)
}
function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId, params: headers }).then((response) => {
if (response.status == 201) {
const index = parties.findIndex((p) => p.id === teamId);
const party = parties[index];
const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index]
party.favorited = true;
party.favorited = true
let clonedParties = clonedeep(parties);
clonedParties[index] = party;
let clonedParties = clonedeep(parties)
clonedParties[index] = party
setParties(clonedParties);
setParties(clonedParties)
}
});
})
}
function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId, params: headers }).then((response) => {
if (response.status == 200) {
const index = parties.findIndex((p) => p.id === teamId);
const party = parties[index];
const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index]
party.favorited = false;
party.favorited = false
let clonedParties = clonedeep(parties);
clonedParties[index] = party;
let clonedParties = clonedeep(parties)
clonedParties[index] = party
setParties(clonedParties);
setParties(clonedParties)
}
});
})
}
// Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true);
else setScrolled(false);
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) {
router.push(`/p/${shortcode}`);
router.push(`/p/${shortcode}`)
}
function renderParties() {
@ -270,14 +270,14 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
onClick={goTo}
onSave={toggleFavorite}
/>
);
});
)
})
}
return (
<div id="Teams">
<Head>
<title>{t("teams.title")}</title>
<title>{t('teams.title')}</title>
<meta property="og:title" content="Discover Teams" />
<meta
@ -303,7 +303,7 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
raidSlug={raidSlug ? raidSlug : undefined}
recency={recency}
>
<h1>{t("teams.title")}</h1>
<h1>{t('teams.title')}</h1>
</FilterBar>
<section>
@ -322,25 +322,25 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
{parties.length == 0 ? (
<div id="NotFound">
<h2>{t("teams.not_found")}</h2>
<h2>{t('teams.not_found')}</h2>
</div>
) : (
""
''
)}
</section>
</div>
);
};
)
}
export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: "string" } },
{ params: { party: 'string' } },
],
fallback: true,
};
};
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -407,31 +407,31 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
const organizeRaids = (raids: Raid[]) => {
// Set up empty raid for "All raids"
const all = {
id: "0",
id: '0',
name: {
en: "All raids",
ja: "全て",
en: 'All raids',
ja: '全て',
},
slug: "all",
slug: 'all',
level: 0,
group: 0,
element: 0,
};
}
const numGroups = Math.max.apply(
Math,
raids.map((raid) => raid.group)
);
let groupedRaids = [];
)
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)
}
return {
raids: raids,
sortedRaids: groupedRaids,
};
};
}
}
export default TeamsRoute;
export default TeamsRoute

View file

@ -1,5 +1,5 @@
interface AccountCookie {
userId: string;
username: string;
token: string;
userId: string
username: string
token: string
}

18
types/AxSkill.d.ts vendored
View file

@ -1,12 +1,12 @@
interface AxSkill {
name: {
[key: string]: string;
en: string;
ja: string;
};
id: number;
minValue: number;
maxValue: number;
suffix?: string;
secondary?: AxSkill[];
[key: string]: string
en: string
ja: string
}
id: number
minValue: number
maxValue: number
suffix?: string
secondary?: AxSkill[]
}

62
types/Character.d.ts vendored
View file

@ -1,40 +1,40 @@
interface Character {
type: "character";
type: 'character'
id: string;
granblue_id: string;
character_id: readonly number[];
element: number;
rarity: number;
gender: number;
max_level: number;
id: string
granblue_id: string
character_id: readonly number[]
element: number
rarity: number
gender: number
max_level: number
name: {
[key: string]: string;
en: string;
ja: string;
};
[key: string]: string
en: string
ja: string
}
hp: {
min_hp: number;
max_hp: number;
max_hp_flb: number;
};
min_hp: number
max_hp: number
max_hp_flb: number
}
atk: {
min_atk: number;
max_atk: number;
max_atk_flb: number;
};
min_atk: number
max_atk: number
max_atk_flb: number
}
uncap: {
flb: boolean;
ulb: boolean;
};
flb: boolean
ulb: boolean
}
race: {
race1: number;
race2: number;
};
race1: number
race2: number
}
proficiency: {
proficiency1: number;
proficiency2: number;
};
position?: number;
special: boolean;
proficiency1: number
proficiency2: number
}
position?: number
special: boolean
}

View file

@ -1,4 +1,4 @@
interface CheckedState {
id: number;
checked: boolean;
id: number
checked: boolean
}

View file

@ -1,10 +1,10 @@
interface ElementState {
[key: string]: CheckedState;
null: CheckedState;
wind: CheckedState;
fire: CheckedState;
water: CheckedState;
earth: CheckedState;
dark: CheckedState;
light: CheckedState;
[key: string]: CheckedState
null: CheckedState
wind: CheckedState
fire: CheckedState
water: CheckedState
earth: CheckedState
dark: CheckedState
light: CheckedState
}

View file

@ -1 +1 @@
type GridArray<T> = { [key: number]: T | undefined };
type GridArray<T> = { [key: number]: T | undefined }

View file

@ -1,6 +1,6 @@
interface GridCharacter {
id: string;
position: number;
object: Character;
uncap_level: number;
id: string
position: number
object: Character
uncap_level: number
}

12
types/GridSummon.d.ts vendored
View file

@ -1,8 +1,8 @@
interface GridSummon {
id: string;
main: boolean;
friend: boolean;
position: number;
object: Summon;
uncap_level: number;
id: string
main: boolean
friend: boolean
position: number
object: Summon
uncap_level: number
}

16
types/GridWeapon.d.ts vendored
View file

@ -1,10 +1,10 @@
interface GridWeapon {
id: string;
mainhand: boolean;
position: number;
object: Weapon;
uncap_level: number;
element: number;
weapon_keys?: Array<WeaponKey>;
ax?: Array<SimpleAxSkill>;
id: string
mainhand: boolean
position: number
object: Weapon
uncap_level: number
element: number
weapon_keys?: Array<WeaponKey>
ax?: Array<SimpleAxSkill>
}

24
types/Job.d.ts vendored
View file

@ -1,16 +1,16 @@
interface Job {
id: string;
row: string;
ml: boolean;
order: number;
id: string
row: string
ml: boolean
order: number
name: {
[key: string]: string;
en: string;
ja: string;
};
[key: string]: string
en: string
ja: string
}
proficiency: {
proficiency1: number;
proficiency2: number;
};
base_job?: Job;
proficiency1: number
proficiency2: number
}
base_job?: Job
}

26
types/JobSkill.d.ts vendored
View file

@ -1,16 +1,16 @@
interface JobSkill {
id: string;
job: Job;
id: string
job: Job
name: {
[key: string]: string;
en: string;
ja: string;
};
slug: string;
color: number;
main: boolean;
base: boolean;
sub: boolean;
emp: boolean;
order: number;
[key: string]: string
en: string
ja: string
}
slug: string
color: number
main: boolean
base: boolean
sub: boolean
emp: boolean
order: number
}

View file

@ -1,3 +1,3 @@
type OnClickEvent = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => void;
) => void

42
types/Party.d.ts vendored
View file

@ -1,25 +1,25 @@
type JobSkillObject = {
[key: number]: JobSkill | undefined;
0: JobSkill | undefined;
1: JobSkill | undefined;
2: JobSkill | undefined;
3: JobSkill | undefined;
};
[key: number]: JobSkill | undefined
0: JobSkill | undefined
1: JobSkill | undefined
2: JobSkill | undefined
3: JobSkill | undefined
}
interface Party {
id: string;
name: string;
description: string;
raid: Raid;
job: Job;
job_skills: JobSkillObject;
shortcode: string;
extra: boolean;
favorited: boolean;
characters: Array<GridCharacter>;
weapons: Array<GridWeapon>;
summons: Array<GridSummon>;
user: User;
created_at: string;
updated_at: string;
id: string
name: string
description: string
raid: Raid
job: Job
job_skills: JobSkillObject
shortcode: string
extra: boolean
favorited: boolean
characters: Array<GridCharacter>
weapons: Array<GridWeapon>
summons: Array<GridSummon>
user: User
created_at: string
updated_at: string
}

View file

@ -1,13 +1,13 @@
interface ProficiencyState {
[key: string]: CheckedState;
sabre: CheckedState;
dagger: CheckedState;
spear: CheckedState;
axe: CheckedState;
staff: CheckedState;
melee: CheckedState;
gun: CheckedState;
bow: CheckedState;
harp: CheckedState;
katana: CheckedState;
[key: string]: CheckedState
sabre: CheckedState
dagger: CheckedState
spear: CheckedState
axe: CheckedState
staff: CheckedState
melee: CheckedState
gun: CheckedState
bow: CheckedState
harp: CheckedState
katana: CheckedState
}

18
types/Raid.d.ts vendored
View file

@ -1,12 +1,12 @@
interface Raid {
id: string;
id: string
name: {
[key: string]: string;
en: string;
ja: string;
};
slug: string;
level: number;
group: number;
element: number;
[key: string]: string
en: string
ja: string
}
slug: string
level: number
group: number
element: number
}

View file

@ -1,5 +1,5 @@
interface RarityState {
[key: string]: CheckedState;
sr: CheckedState;
ssr: CheckedState;
[key: string]: CheckedState
sr: CheckedState
ssr: CheckedState
}

View file

@ -1,4 +1,4 @@
interface SimpleAxSkill {
modifier: number;
strength: number;
modifier: number
strength: number
}

46
types/Summon.d.ts vendored
View file

@ -1,30 +1,30 @@
interface Summon {
type: "summon";
type: 'summon'
id: string;
granblue_id: number;
element: number;
max_level: number;
id: string
granblue_id: number
element: number
max_level: number
name: {
[key: string]: string;
en: string;
ja: string;
};
[key: string]: string
en: string
ja: string
}
hp: {
min_hp: number;
max_hp: number;
max_hp_flb: number;
max_hp_ulb: number;
};
min_hp: number
max_hp: number
max_hp_flb: number
max_hp_ulb: number
}
atk: {
min_atk: number;
max_atk: number;
max_atk_flb: number;
max_atk_ulb: number;
};
min_atk: number
max_atk: number
max_atk_flb: number
max_atk_ulb: number
}
uncap: {
flb: boolean;
ulb: boolean;
};
position?: number;
flb: boolean
ulb: boolean
}
position?: number
}

View file

@ -1,7 +1,7 @@
interface TeamElement {
id: number;
id: number
name: {
en: string;
ja: string;
};
en: string
ja: string
}
}

16
types/User.d.ts vendored
View file

@ -1,11 +1,11 @@
interface User {
id: string;
username: string;
granblueId: number;
id: string
username: string
granblueId: number
picture: {
picture: string;
element: string;
};
gender: number;
private: boolean;
picture: string
element: string
}
gender: number
private: boolean
}

View file

@ -1,6 +1,6 @@
interface UserCookie {
picture: string;
element: string;
language: string;
gender: number;
picture: string
element: string
language: string
gender: number
}

54
types/Weapon.d.ts vendored
View file

@ -1,34 +1,34 @@
interface Weapon {
type: "weapon";
type: 'weapon'
id: string;
granblue_id: number;
element: number;
proficiency: number;
max_level: number;
max_skill_level: number;
series: number;
ax: number;
id: string
granblue_id: number
element: number
proficiency: number
max_level: number
max_skill_level: number
series: number
ax: number
name: {
[key: string]: string;
en: string;
ja: string;
};
[key: string]: string
en: string
ja: string
}
hp: {
min_hp: number;
max_hp: number;
max_hp_flb: number;
max_hp_ulb: number;
};
min_hp: number
max_hp: number
max_hp_flb: number
max_hp_ulb: number
}
atk: {
min_atk: number;
max_atk: number;
max_atk_flb: number;
max_atk_ulb: number;
};
min_atk: number
max_atk: number
max_atk_flb: number
max_atk_ulb: number
}
uncap: {
flb: boolean;
ulb: boolean;
};
position?: number;
flb: boolean
ulb: boolean
}
position?: number
}

18
types/WeaponKey.d.ts vendored
View file

@ -1,12 +1,12 @@
interface WeaponKey {
id: string;
id: string
name: {
[key: string]: string;
en: string;
ja: string;
};
series: integer;
slot: integer;
group: integer;
order: integer;
[key: string]: string
en: string
ja: string
}
series: integer
slot: integer
group: integer
order: integer
}

View file

@ -1,27 +1,27 @@
interface WeaponSeriesState {
[key: string]: CheckedState;
seraphic: CheckedState;
grand: CheckedState;
opus: CheckedState;
draconic: CheckedState;
ultima: CheckedState;
bahamut: CheckedState;
omega: CheckedState;
primal: CheckedState;
olden_primal: CheckedState;
militis: CheckedState;
beast: CheckedState;
rose: CheckedState;
xeno: CheckedState;
hollowsky: CheckedState;
astral: CheckedState;
epic: CheckedState;
ennead: CheckedState;
cosmos: CheckedState;
ancestral: CheckedState;
superlative: CheckedState;
vintage: CheckedState;
class_champion: CheckedState;
sephira: CheckedState;
new_world: CheckedState;
[key: string]: CheckedState
seraphic: CheckedState
grand: CheckedState
opus: CheckedState
draconic: CheckedState
ultima: CheckedState
bahamut: CheckedState
omega: CheckedState
primal: CheckedState
olden_primal: CheckedState
militis: CheckedState
beast: CheckedState
rose: CheckedState
xeno: CheckedState
hollowsky: CheckedState
astral: CheckedState
epic: CheckedState
ennead: CheckedState
cosmos: CheckedState
ancestral: CheckedState
superlative: CheckedState
vintage: CheckedState
class_champion: CheckedState
sephira: CheckedState
new_world: CheckedState
}

View file

@ -1,2 +1,2 @@
declare module "*.jpg";
declare module "*.svg";
declare module '*.jpg'
declare module '*.svg'

16
types/index.d.ts vendored
View file

@ -1,9 +1,9 @@
export type SearchableObject = Character | Weapon | Summon | JobSkill;
export type SearchableObjectArray = (Character | Weapon | Summon | JobSkill)[];
export type SearchableObject = Character | Weapon | Summon | JobSkill
export type SearchableObjectArray = (Character | Weapon | Summon | JobSkill)[]
export type JobSkillObject = {
[key: number]: JobSkill | undefined;
0: JobSkill | undefined;
1: JobSkill | undefined;
2: JobSkill | undefined;
3: JobSkill | undefined;
};
[key: number]: JobSkill | undefined
0: JobSkill | undefined
1: JobSkill | undefined
2: JobSkill | undefined
3: JobSkill | undefined
}

View file

@ -1,59 +1,59 @@
export const allElement: TeamElement = {
id: -1,
name: {
en: "All",
ja: "全s",
en: 'All',
ja: '全s',
},
};
}
export const elements: TeamElement[] = [
{
id: 0,
name: {
en: "Null",
ja: "無",
en: 'Null',
ja: '無',
},
},
{
id: 1,
name: {
en: "Wind",
ja: "風",
en: 'Wind',
ja: '風',
},
},
{
id: 2,
name: {
en: "Fire",
ja: "火",
en: 'Fire',
ja: '火',
},
},
{
id: 3,
name: {
en: "Water",
ja: "水",
en: 'Water',
ja: '水',
},
},
{
id: 4,
name: {
en: "Earth",
ja: "土",
en: 'Earth',
ja: '土',
},
},
{
id: 5,
name: {
en: "Dark",
ja: "闇",
en: 'Dark',
ja: '闇',
},
},
{
id: 6,
name: {
en: "Light",
ja: "光",
en: 'Light',
ja: '光',
},
},
];
]

View file

@ -1,20 +1,20 @@
import { proxy } from "valtio";
import { proxy } from 'valtio'
interface AccountState {
[key: string]: any;
[key: string]: any
account: {
authorized: boolean;
authorized: boolean
user:
| {
id: string;
username: string;
picture: string;
element: string;
gender: number;
id: string
username: string
picture: string
element: string
gender: number
}
| undefined
}
| undefined;
};
}
export const initialAccountState: AccountState = {
@ -22,6 +22,6 @@ export const initialAccountState: AccountState = {
authorized: false,
user: undefined,
},
};
}
export const accountState = proxy(initialAccountState);
export const accountState = proxy(initialAccountState)

View file

@ -1,62 +1,62 @@
import { proxy } from "valtio";
import { JobSkillObject } from "~types";
import { proxy } from 'valtio'
import { JobSkillObject } from '~types'
const emptyJob: Job = {
id: "-1",
row: "",
id: '-1',
row: '',
ml: false,
order: 0,
name: {
en: "",
ja: "",
en: '',
ja: '',
},
proficiency: {
proficiency1: 0,
proficiency2: 0,
},
};
}
interface AppState {
[key: string]: any;
[key: string]: any
party: {
id: string | undefined;
editable: boolean;
detailsVisible: boolean;
name: string | undefined;
description: string | undefined;
job: Job;
jobSkills: JobSkillObject;
raid: Raid | undefined;
element: number;
extra: boolean;
user: User | undefined;
favorited: boolean;
created_at: string;
updated_at: string;
};
id: string | undefined
editable: boolean
detailsVisible: boolean
name: string | undefined
description: string | undefined
job: Job
jobSkills: JobSkillObject
raid: Raid | undefined
element: number
extra: boolean
user: User | undefined
favorited: boolean
created_at: string
updated_at: string
}
grid: {
weapons: {
mainWeapon: GridWeapon | undefined;
allWeapons: GridArray<GridWeapon>;
};
mainWeapon: GridWeapon | undefined
allWeapons: GridArray<GridWeapon>
}
summons: {
mainSummon: GridSummon | undefined;
friendSummon: GridSummon | undefined;
allSummons: GridArray<GridSummon>;
};
characters: GridArray<GridCharacter>;
};
mainSummon: GridSummon | undefined
friendSummon: GridSummon | undefined
allSummons: GridArray<GridSummon>
}
characters: GridArray<GridCharacter>
}
search: {
recents: {
characters: Character[];
weapons: Weapon[];
summons: Summon[];
};
};
raids: Raid[];
jobs: Job[];
jobSkills: JobSkill[];
characters: Character[]
weapons: Weapon[]
summons: Summon[]
}
}
raids: Raid[]
jobs: Job[]
jobSkills: JobSkill[]
}
export const initialAppState: AppState = {
@ -103,6 +103,6 @@ export const initialAppState: AppState = {
raids: [],
jobs: [],
jobSkills: [],
};
}
export const appState = proxy(initialAppState);
export const appState = proxy(initialAppState)

View file

@ -2,100 +2,100 @@ export const axData: AxSkill[][] = [
[
{
name: {
en: "ATK",
ja: "攻撃",
en: 'ATK',
ja: '攻撃',
},
id: 0,
minValue: 1,
maxValue: 3.5,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Double Attack Rate",
ja: "DA確率",
en: 'Double Attack Rate',
ja: 'DA確率',
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Triple Attack Rate",
ja: "TA確率",
en: 'Triple Attack Rate',
ja: 'TA確率',
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Skill DMG Cap",
ja: "アビ上限",
en: 'Skill DMG Cap',
ja: 'アビ上限',
},
id: 7,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
],
},
{
name: {
en: "DEF",
ja: "防御",
en: 'DEF',
ja: '防御',
},
id: 1,
minValue: 1,
maxValue: 8,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "HP",
ja: "HP",
en: 'HP',
ja: 'HP',
},
id: 2,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Debuff Resistance",
ja: "弱体耐性",
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Healing",
ja: "回復性能",
en: 'Healing',
ja: '回復性能',
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Enmity",
ja: "背水",
en: 'Enmity',
ja: '背水',
},
id: 11,
minValue: 1,
@ -105,48 +105,48 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "HP",
ja: "HP",
en: 'HP',
ja: 'HP',
},
id: 2,
minValue: 1,
maxValue: 11,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "DEF",
ja: "防御",
en: 'DEF',
ja: '防御',
},
id: 1,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Debuff Resistance",
ja: "弱体耐性",
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Healing",
ja: "回復性能",
en: 'Healing',
ja: '回復性能',
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -156,48 +156,48 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "ATK",
ja: "攻撃",
en: 'ATK',
ja: '攻撃',
},
id: 0,
minValue: 1,
maxValue: 1.5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Elemental ATK",
ja: "全属性攻撃力",
en: 'Elemental ATK',
ja: '全属性攻撃力',
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "C.A. DMG Cap",
ja: "奥義上限",
en: 'C.A. DMG Cap',
ja: '奥義上限',
},
id: 8,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -207,53 +207,53 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Multiattack Rate",
ja: "連撃率",
en: 'Multiattack Rate',
ja: '連撃率',
},
id: 4,
minValue: 1,
maxValue: 4,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Elemental ATK",
ja: "全属性攻撃力",
en: 'Elemental ATK',
ja: '全属性攻撃力',
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Double Attack Rate",
ja: "DA確率",
en: 'Double Attack Rate',
ja: 'DA確率',
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Triple Attack Rate",
ja: "TA確率",
en: 'Triple Attack Rate',
ja: 'TA確率',
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
],
},
@ -261,48 +261,48 @@ export const axData: AxSkill[][] = [
[
{
name: {
en: "ATK",
ja: "攻撃",
en: 'ATK',
ja: '攻撃',
},
id: 0,
minValue: 1,
maxValue: 3.5,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Multiattack Rate",
ja: "連撃確率",
en: 'Multiattack Rate',
ja: '連撃確率',
},
id: 4,
minValue: 1.5,
maxValue: 4,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Normal ATK DMG Cap",
ja: "通常ダメ上限",
en: 'Normal ATK DMG Cap',
ja: '通常ダメ上限',
},
id: 14,
minValue: 0.5,
maxValue: 1.5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Supplemental Skill DMG",
ja: "アビ与ダメ上昇",
en: 'Supplemental Skill DMG',
ja: 'アビ与ダメ上昇',
},
id: 15,
minValue: 1,
@ -312,48 +312,48 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "DEF",
ja: "防御",
en: 'DEF',
ja: '防御',
},
id: 1,
minValue: 1,
maxValue: 8,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "Elemental DMG Reduction",
ja: "属性ダメ軽減",
en: 'Elemental DMG Reduction',
ja: '属性ダメ軽減',
},
id: 17,
minValue: 1,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Debuff Resistance",
ja: "弱体耐性",
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Healing",
ja: "回復性能",
en: 'Healing',
ja: '回復性能',
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Enmity",
ja: "背水",
en: 'Enmity',
ja: '背水',
},
id: 11,
minValue: 1,
@ -363,48 +363,48 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "HP",
ja: "HP",
en: 'HP',
ja: 'HP',
},
id: 2,
minValue: 1,
maxValue: 11,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "Elemental DMG Reduction",
ja: "属性ダメ軽減",
en: 'Elemental DMG Reduction',
ja: '属性ダメ軽減',
},
id: 17,
minValue: 1,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Debuff Resistance",
ja: "弱体耐性",
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Healing",
ja: "回復性能",
en: 'Healing',
ja: '回復性能',
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -414,28 +414,28 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "Multiattack Rate",
ja: "連撃率",
en: 'Multiattack Rate',
ja: '連撃率',
},
id: 4,
minValue: 1.5,
maxValue: 4,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Supplemental Skill DMG",
ja: "アビ与ダメ上昇",
en: 'Supplemental Skill DMG',
ja: 'アビ与ダメ上昇',
},
id: 15,
minValue: 1,
@ -443,8 +443,8 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Supplemental C.A. DMG",
ja: "奥義与ダメ上昇",
en: 'Supplemental C.A. DMG',
ja: '奥義与ダメ上昇',
},
id: 16,
minValue: 1,
@ -452,8 +452,8 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -463,18 +463,18 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Multiattack Rate",
ja: "連撃率",
en: 'Multiattack Rate',
ja: '連撃率',
},
id: 4,
minValue: 1,
maxValue: 4,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "Supplemental C.A. DMG",
ja: "奥義与ダメ上昇",
en: 'Supplemental C.A. DMG',
ja: '奥義与ダメ上昇',
},
id: 16,
minValue: 1,
@ -482,18 +482,18 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Normal ATK DMG Cap",
ja: "通常ダメ上限",
en: 'Normal ATK DMG Cap',
ja: '通常ダメ上限',
},
id: 14,
minValue: 0.5,
maxValue: 1.5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -501,8 +501,8 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Enmity",
ja: "背水",
en: 'Enmity',
ja: '背水',
},
id: 11,
minValue: 1,
@ -514,100 +514,100 @@ export const axData: AxSkill[][] = [
[
{
name: {
en: "ATK",
ja: "攻撃",
en: 'ATK',
ja: '攻撃',
},
id: 0,
minValue: 1,
maxValue: 3.5,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Double Attack Rate",
ja: "DA確率",
en: 'Double Attack Rate',
ja: 'DA確率',
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Triple Attack Rate",
ja: "TA確率",
en: 'Triple Attack Rate',
ja: 'TA確率',
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Skill DMG Cap",
ja: "アビ上限",
en: 'Skill DMG Cap',
ja: 'アビ上限',
},
id: 7,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
],
},
{
name: {
en: "DEF",
ja: "防御",
en: 'DEF',
ja: '防御',
},
id: 1,
minValue: 1,
maxValue: 8,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "HP",
ja: "HP",
en: 'HP',
ja: 'HP',
},
id: 2,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Debuff Resistance",
ja: "弱体耐性",
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Healing",
ja: "回復性能",
en: 'Healing',
ja: '回復性能',
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Enmity",
ja: "背水",
en: 'Enmity',
ja: '背水',
},
id: 11,
minValue: 1,
@ -617,48 +617,48 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "HP",
ja: "HP",
en: 'HP',
ja: 'HP',
},
id: 2,
minValue: 1,
maxValue: 11,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "DEF",
ja: "防御",
en: 'DEF',
ja: '防御',
},
id: 1,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Debuff Resistance",
ja: "弱体耐性",
en: 'Debuff Resistance',
ja: '弱体耐性',
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Healing",
ja: "回復性能",
en: 'Healing',
ja: '回復性能',
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -668,48 +668,48 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "ATK",
ja: "攻撃",
en: 'ATK',
ja: '攻撃',
},
id: 0,
minValue: 1,
maxValue: 1.5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Elemental ATK",
ja: "全属性攻撃力",
en: 'Elemental ATK',
ja: '全属性攻撃力',
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "C.A. DMG Cap",
ja: "奥義上限",
en: 'C.A. DMG Cap',
ja: '奥義上限',
},
id: 8,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Stamina",
ja: "渾身",
en: 'Stamina',
ja: '渾身',
},
id: 12,
minValue: 1,
@ -719,75 +719,75 @@ export const axData: AxSkill[][] = [
},
{
name: {
en: "Multiattack Rate",
ja: "連撃率",
en: 'Multiattack Rate',
ja: '連撃率',
},
id: 4,
minValue: 1,
maxValue: 4,
suffix: "%",
suffix: '%',
secondary: [
{
name: {
en: "C.A. DMG",
ja: "奥義ダメ",
en: 'C.A. DMG',
ja: '奥義ダメ',
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Elemental ATK",
ja: "全属性攻撃力",
en: 'Elemental ATK',
ja: '全属性攻撃力',
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Double Attack Rate",
ja: "DA確率",
en: 'Double Attack Rate',
ja: 'DA確率',
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Triple Attack Rate",
ja: "TA確率",
en: 'Triple Attack Rate',
ja: 'TA確率',
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: "%",
suffix: '%',
},
],
},
{
name: {
en: "EXP Up",
ja: "EXP UP",
en: 'EXP Up',
ja: 'EXP UP',
},
id: 18,
minValue: 5,
maxValue: 10,
suffix: "%",
suffix: '%',
},
{
name: {
en: "Rupies",
ja: "獲得ルピ",
en: 'Rupies',
ja: '獲得ルピ',
},
id: 19,
minValue: 10,
maxValue: 20,
suffix: "%",
suffix: '%',
},
],
];
]

View file

@ -7,7 +7,7 @@ export const emptyRarityState: RarityState = {
id: 3,
checked: true,
},
};
}
export const emptyElementState: ElementState = {
null: {
@ -38,7 +38,7 @@ export const emptyElementState: ElementState = {
id: 6,
checked: false,
},
};
}
export const emptyProficiencyState: ProficiencyState = {
sabre: {
@ -81,7 +81,7 @@ export const emptyProficiencyState: ProficiencyState = {
id: 10,
checked: false,
},
};
}
export const emptyWeaponSeriesState: WeaponSeriesState = {
seraphic: {
@ -184,4 +184,4 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 29,
checked: false,
},
};
}

View file

@ -1,60 +1,60 @@
interface JobGroup {
slug: string;
slug: string
name: {
[key: string]: string;
en: string;
ja: string;
};
[key: string]: string
en: string
ja: string
}
}
export const jobGroups: JobGroup[] = [
{
slug: "1",
slug: '1',
name: {
en: "Row I",
ja: "Class I",
en: 'Row I',
ja: 'Class I',
},
},
{
slug: "2",
slug: '2',
name: {
en: "Row II",
ja: "Class II",
en: 'Row II',
ja: 'Class II',
},
},
{
slug: "3",
slug: '3',
name: {
en: "Row III",
ja: "Class III",
en: 'Row III',
ja: 'Class III',
},
},
{
slug: "4",
slug: '4',
name: {
en: "Row IV",
ja: "Class IV",
en: 'Row IV',
ja: 'Class IV',
},
},
{
slug: "5",
slug: '5',
name: {
en: "Row V",
ja: "Class V",
en: 'Row V',
ja: 'Class V',
},
},
{
slug: "ex1",
slug: 'ex1',
name: {
en: "Extra I",
ja: "EXTRA I",
en: 'Extra I',
ja: 'EXTRA I',
},
},
{
slug: "ex2",
slug: 'ex2',
name: {
en: "Extra II",
ja: "EXTRA II",
en: 'Extra II',
ja: 'EXTRA II',
},
},
];
]

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