Run prettier on src

This commit is contained in:
Justin Edmund 2022-12-04 07:19:31 -08:00
parent 3ccb80d33b
commit efa864fb80
142 changed files with 8617 additions and 7923 deletions

View file

@ -1,24 +1,29 @@
import React from 'react' import React from "react";
import { useTranslation } from 'next-i18next' import { useTranslation } from "next-i18next";
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from "@radix-ui/react-dialog";
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg";
import './index.scss' import "./index.scss";
const AboutModal = () => { const AboutModal = () => {
const { t } = useTranslation('common') const { t } = useTranslation("common");
return ( return (
<Dialog.Root> <Dialog.Root>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('modals.about.title')}</span> <span>{t("modals.about.title")}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
className="About Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">{t('menu.about')}</Dialog.Title> <Dialog.Title className="DialogTitle">
{t("menu.about")}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />
@ -28,20 +33,27 @@ const AboutModal = () => {
<section> <section>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
Granblue.team is a tool to save and share team compositions for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a> Granblue.team is a tool to save and share team compositions for{" "}
<a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
</Dialog.Description> </Dialog.Description>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
Start adding things to a team and a URL will be created for you to share it wherever you like, no account needed. Start adding things to a team and a URL will be created for you to
share it wherever you like, no account needed.
</Dialog.Description> </Dialog.Description>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
You can make an account to save any teams you find for future reference, or to keep all of your teams together in one place. You can make an account to save any teams you find for future
reference, or to keep all of your teams together in one place.
</Dialog.Description> </Dialog.Description>
</section> </section>
<section> <section>
<Dialog.Title className="DialogTitle">Credits</Dialog.Title> <Dialog.Title className="DialogTitle">Credits</Dialog.Title>
<Dialog.Description className="DialogDescription"> <Dialog.Description className="DialogDescription">
Granblue.team was built by <a href="https://twitter.com/jedmund">@jedmund</a> with a lot of help from <a href="https://twitter.com/lalalalinna">@lalalalinna</a> and <a href="https://twitter.com/tarngerine">@tarngerine</a>. Granblue.team was built by{" "}
<a href="https://twitter.com/jedmund">@jedmund</a> with a lot of
help from{" "}
<a href="https://twitter.com/lalalalinna">@lalalalinna</a> and{" "}
<a href="https://twitter.com/tarngerine">@tarngerine</a>.
</Dialog.Description> </Dialog.Description>
</section> </section>
@ -55,7 +67,7 @@ const AboutModal = () => {
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
) );
} };
export default AboutModal export default AboutModal;

View file

@ -75,7 +75,7 @@
gap: $unit * 2; gap: $unit * 2;
select { 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-y: center;
background-position-x: 95%; background-position-x: 95%;
margin: 0; margin: 0;

View file

@ -1,33 +1,35 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import * as Dialog from "@radix-ui/react-dialog" import * as Dialog from "@radix-ui/react-dialog";
import * as Switch from "@radix-ui/react-switch" import * as Switch from "@radix-ui/react-switch";
import api from "~utils/api" import api from "~utils/api";
import { accountState } from "~utils/accountState" import { accountState } from "~utils/accountState";
import { pictureData } from "~utils/pictureData" import { pictureData } from "~utils/pictureData";
import Button from "~components/Button" import Button from "~components/Button";
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from "~public/icons/Cross.svg";
import "./index.scss" import "./index.scss";
const AccountModal = () => { const AccountModal = () => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState);
const router = useRouter() const router = useRouter();
const { t } = useTranslation("common") const { t } = useTranslation("common");
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie("account");
const headers = {} const headers = {};
// cookies.account != null // cookies.account != null
// ? { // ? {
// headers: { // headers: {
@ -37,17 +39,17 @@ const AccountModal = () => {
// : {} // : {}
// State // State
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false);
const [picture, setPicture] = useState("") const [picture, setPicture] = useState("");
const [language, setLanguage] = useState("") const [language, setLanguage] = useState("");
const [gender, setGender] = useState(0) const [gender, setGender] = useState(0);
const [privateProfile, setPrivateProfile] = useState(false) const [privateProfile, setPrivateProfile] = useState(false);
// Refs // Refs
const pictureSelect = React.createRef<HTMLSelectElement>() const pictureSelect = React.createRef<HTMLSelectElement>();
const languageSelect = React.createRef<HTMLSelectElement>() const languageSelect = React.createRef<HTMLSelectElement>();
const genderSelect = React.createRef<HTMLSelectElement>() const genderSelect = React.createRef<HTMLSelectElement>();
const privateSelect = React.createRef<HTMLInputElement>() const privateSelect = React.createRef<HTMLInputElement>();
// useEffect(() => { // useEffect(() => {
// if (cookies.user) setPicture(cookies.user.picture) // if (cookies.user) setPicture(cookies.user.picture)
@ -62,27 +64,27 @@ const AccountModal = () => {
<option key={`picture-${i}`} value={item.filename}> <option key={`picture-${i}`} value={item.filename}>
{item.name[locale]} {item.name[locale]}
</option> </option>
) );
}) });
function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) { 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>) { 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>) { 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) { function handlePrivateChange(checked: boolean) {
setPrivateProfile(checked) setPrivateProfile(checked);
} }
function update(event: React.FormEvent<HTMLFormElement>) { function update(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault() event.preventDefault();
const object = { const object = {
user: { user: {
@ -92,7 +94,7 @@ const AccountModal = () => {
gender: gender, gender: gender,
private: privateProfile, private: privateProfile,
}, },
} };
// api.endpoints.users // api.endpoints.users
// .update(cookies.account.user_id, object, headers) // .update(cookies.account.user_id, object, headers)
@ -129,7 +131,7 @@ const AccountModal = () => {
} }
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open);
} }
return ( return (
@ -249,7 +251,7 @@ const AccountModal = () => {
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
) );
} };
export default AccountModal export default AccountModal;

View file

@ -1,19 +1,19 @@
import React from "react" import React from "react";
import * as AlertDialog from "@radix-ui/react-alert-dialog" import * as AlertDialog from "@radix-ui/react-alert-dialog";
import "./index.scss" import "./index.scss";
import Button from "~components/Button" import Button from "~components/Button";
import { ButtonType } from "~utils/enums" import { ButtonType } from "~utils/enums";
// Props // Props
interface Props { interface Props {
open: boolean open: boolean;
title?: string title?: string;
message: string message: string;
primaryAction?: () => void primaryAction?: () => void;
primaryActionText?: string primaryActionText?: string;
cancelAction: () => void cancelAction: () => void;
cancelActionText: string cancelActionText: string;
} }
const Alert = (props: Props) => { const Alert = (props: Props) => {
@ -45,7 +45,7 @@ const Alert = (props: Props) => {
</div> </div>
</AlertDialog.Portal> </AlertDialog.Portal>
</AlertDialog.Root> </AlertDialog.Root>
) );
} };
export default Alert export default Alert;

View file

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

View file

@ -35,28 +35,28 @@
} }
&.save:hover { &.save:hover {
color: #FF4D4D; color: #ff4d4d;
.icon svg { .icon svg {
fill: #FF4D4D; fill: #ff4d4d;
stroke: #FF4D4D; stroke: #ff4d4d;
} }
} }
&.save.Active { &.save.Active {
color: #FF4D4D; color: #ff4d4d;
.icon svg { .icon svg {
fill: #FF4D4D; fill: #ff4d4d;
stroke: #FF4D4D; stroke: #ff4d4d;
} }
&:hover { &:hover {
color: darken(#FF4D4D, 30); color: darken(#ff4d4d, 30);
.icon svg { .icon svg {
fill: darken(#FF4D4D, 30); fill: darken(#ff4d4d, 30);
stroke: darken(#FF4D4D, 30); stroke: darken(#ff4d4d, 30);
} }
} }
} }
@ -69,7 +69,7 @@
color: $error; color: $error;
&:hover { &:hover {
color: darken($error, 10) color: darken($error, 10);
} }
} }
@ -108,8 +108,8 @@
color: #8b8b8b; color: #8b8b8b;
&:hover { &:hover {
background: #4B9BE5; background: #4b9be5;
color: #233E56; color: #233e56;
} }
} }
@ -196,7 +196,6 @@
} }
} }
&.light { &.light {
background: $light-bg-light; background: $light-bg-light;
color: $light-text-dark; color: $light-text-dark;

View file

@ -1,124 +1,143 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react";
import classNames from 'classnames' import classNames from "classnames";
import Link from 'next/link' import Link from "next/link";
import AddIcon from '~public/icons/Add.svg' import AddIcon from "~public/icons/Add.svg";
import CheckIcon from '~public/icons/LargeCheck.svg' import CheckIcon from "~public/icons/LargeCheck.svg";
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg";
import EditIcon from '~public/icons/Edit.svg' import EditIcon from "~public/icons/Edit.svg";
import LinkIcon from '~public/icons/Link.svg' import LinkIcon from "~public/icons/Link.svg";
import MenuIcon from '~public/icons/Menu.svg' import MenuIcon from "~public/icons/Menu.svg";
import SaveIcon from '~public/icons/Save.svg' import SaveIcon from "~public/icons/Save.svg";
import SettingsIcon from '~public/icons/Settings.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 { interface Props {
active?: boolean active?: boolean;
disabled?: boolean disabled?: boolean;
classes?: string[], classes?: string[];
icon?: string icon?: string;
type?: ButtonType type?: ButtonType;
children?: React.ReactNode children?: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLElement>) => void onClick?: (event: React.MouseEvent<HTMLElement>) => void;
} }
const Button = (props: Props) => { const Button = (props: Props) => {
// States // States
const [active, setActive] = useState(false) const [active, setActive] = useState(false);
const [disabled, setDisabled] = useState(false) const [disabled, setDisabled] = useState(false);
const [pressed, setPressed] = useState(false) const [pressed, setPressed] = useState(false);
const [buttonType, setButtonType] = useState(ButtonType.Base) const [buttonType, setButtonType] = useState(ButtonType.Base);
const classes = classNames({ const classes = classNames(
{
Button: true, Button: true,
'Active': active, Active: active,
'btn-pressed': pressed, "btn-pressed": pressed,
'btn-disabled': disabled, "btn-disabled": disabled,
'save': props.icon === 'save', save: props.icon === "save",
'destructive': props.type == ButtonType.Destructive destructive: props.type == ButtonType.Destructive,
}, props.classes) },
props.classes
);
useEffect(() => { useEffect(() => {
if (props.active) setActive(props.active) if (props.active) setActive(props.active);
if (props.disabled) setDisabled(props.disabled) if (props.disabled) setDisabled(props.disabled);
if (props.type) setButtonType(props.type) if (props.type) setButtonType(props.type);
}, [props.active, props.disabled, props.type]) }, [props.active, props.disabled, props.type]);
const addIcon = ( const addIcon = (
<span className='icon'> <span className="icon">
<AddIcon /> <AddIcon />
</span> </span>
) );
const menuIcon = ( const menuIcon = (
<span className='icon'> <span className="icon">
<MenuIcon /> <MenuIcon />
</span> </span>
) );
const linkIcon = ( const linkIcon = (
<span className='icon stroke'> <span className="icon stroke">
<LinkIcon /> <LinkIcon />
</span> </span>
) );
const checkIcon = ( const checkIcon = (
<span className='icon check'> <span className="icon check">
<CheckIcon /> <CheckIcon />
</span> </span>
) );
const crossIcon = ( const crossIcon = (
<span className='icon'> <span className="icon">
<CrossIcon /> <CrossIcon />
</span> </span>
) );
const editIcon = ( const editIcon = (
<span className='icon'> <span className="icon">
<EditIcon /> <EditIcon />
</span> </span>
) );
const saveIcon = ( const saveIcon = (
<span className='icon stroke'> <span className="icon stroke">
<SaveIcon /> <SaveIcon />
</span> </span>
) );
const settingsIcon = ( const settingsIcon = (
<span className='icon settings'> <span className="icon settings">
<SettingsIcon /> <SettingsIcon />
</span> </span>
) );
function getIcon() { function getIcon() {
let icon: React.ReactNode let icon: React.ReactNode;
switch(props.icon) { switch (props.icon) {
case 'new': icon = addIcon; break case "new":
case 'menu': icon = menuIcon; break icon = addIcon;
case 'link': icon = linkIcon; break break;
case 'check': icon = checkIcon; break case "menu":
case 'cross': icon = crossIcon; break icon = menuIcon;
case 'edit': icon = editIcon; break break;
case 'save': icon = saveIcon; break case "link":
case 'settings': icon = settingsIcon; break 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() { function handleMouseDown() {
setPressed(true) setPressed(true);
} }
function handleMouseUp() { function handleMouseUp() {
setPressed(false) setPressed(false);
} }
return ( return (
<button <button
@ -126,16 +145,17 @@ const Button = (props: Props) => {
disabled={disabled} disabled={disabled}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
onClick={props.onClick}> onClick={props.onClick}
{ getIcon() } >
{getIcon()}
{ (props.type != ButtonType.IconOnly) ? {props.type != ButtonType.IconOnly ? (
<span className='text'> <span className="text">{props.children}</span>
{ props.children } ) : (
</span> : '' ""
} )}
</button> </button>
) );
} };
export default Button export default Button;

View file

@ -1,28 +1,33 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react";
import './index.scss' import "./index.scss";
interface Props { interface Props {
fieldName: string fieldName: string;
placeholder: string placeholder: string;
value?: string value?: string;
limit: number limit: number;
error: string error: string;
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function useFieldSet(props, ref) { const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text' function useFieldSet(props, ref) {
const fieldType = ["password", "confirm_password"].includes(props.fieldName)
? "password"
: "text";
const [currentCount, setCurrentCount] = useState(0) const [currentCount, setCurrentCount] = useState(0);
useEffect(() => { useEffect(() => {
setCurrentCount((props.value) ? props.limit - props.value.length : props.limit) setCurrentCount(
}, [props.limit, props.value]) props.value ? props.limit - props.value.length : props.limit
);
}, [props.limit, props.value]);
function onChange(event: React.ChangeEvent<HTMLInputElement>) { function onChange(event: React.ChangeEvent<HTMLInputElement>) {
setCurrentCount(props.limit - event.currentTarget.value.length) setCurrentCount(props.limit - event.currentTarget.value.length);
if (props.onChange) props.onChange(event) if (props.onChange) props.onChange(event);
} }
return ( return (
@ -34,7 +39,7 @@ const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function u
type={fieldType} type={fieldType}
name={props.fieldName} name={props.fieldName}
placeholder={props.placeholder} placeholder={props.placeholder}
defaultValue={props.value || ''} defaultValue={props.value || ""}
onBlur={props.onBlur} onBlur={props.onBlur}
onChange={onChange} onChange={onChange}
maxLength={props.limit} maxLength={props.limit}
@ -43,12 +48,10 @@ const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function u
/> />
<span className="Counter">{currentCount}</span> <span className="Counter">{currentCount}</span>
</div> </div>
{ {props.error.length > 0 && <p className="InputError">{props.error}</p>}
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset> </fieldset>
) );
}) }
);
export default CharLimitedFieldset export default CharLimitedFieldset;

View file

@ -1,70 +1,70 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { setCookie } from "cookies-next" import { setCookie } from "cookies-next";
import Router, { useRouter } from "next/router" import Router, { useRouter } from "next/router";
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next";
import { AxiosResponse } from "axios" import { AxiosResponse } from "axios";
import * as Dialog from "@radix-ui/react-dialog" import * as Dialog from "@radix-ui/react-dialog";
import api from "~utils/api" import api from "~utils/api";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import { accountState } from "~utils/accountState" import { accountState } from "~utils/accountState";
import Button from "~components/Button" import Button from "~components/Button";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
open: boolean open: boolean;
incomingCharacter?: Character incomingCharacter?: Character;
conflictingCharacters?: GridCharacter[] conflictingCharacters?: GridCharacter[];
desiredPosition: number desiredPosition: number;
resolveConflict: () => void resolveConflict: () => void;
resetConflict: () => void resetConflict: () => void;
} }
const CharacterConflictModal = (props: Props) => { const CharacterConflictModal = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation("common");
// States // States
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
setOpen(props.open) setOpen(props.open);
}, [setOpen, props.open]) }, [setOpen, props.open]);
function imageUrl(character?: Character, uncap: number = 0) { function imageUrl(character?: Character, uncap: number = 0) {
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = "01" let suffix = "01";
if (uncap == 6) suffix = "04" if (uncap == 6) suffix = "04";
else if (uncap == 5) suffix = "03" else if (uncap == 5) suffix = "03";
else if (uncap > 2) suffix = "02" else if (uncap > 2) suffix = "02";
// Special casing for Lyria (and Young Cat eventually) // Special casing for Lyria (and Young Cat eventually)
if (character?.granblue_id === "3030182000") { if (character?.granblue_id === "3030182000") {
let element = 1 let element = 1;
if ( if (
appState.grid.weapons.mainWeapon && appState.grid.weapons.mainWeapon &&
appState.grid.weapons.mainWeapon.element appState.grid.weapons.mainWeapon.element
) { ) {
element = appState.grid.weapons.mainWeapon.element element = appState.grid.weapons.mainWeapon.element;
} else if (appState.party.element != 0) { } 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) { function openChange(open: boolean) {
setOpen(open) setOpen(open);
} }
function close() { function close() {
setOpen(false) setOpen(false);
props.resetConflict() props.resetConflict();
} }
return ( return (
@ -107,7 +107,7 @@ const CharacterConflictModal = (props: Props) => {
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
) );
} };
export default CharacterConflictModal export default CharacterConflictModal;

View file

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

View file

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

View file

@ -37,11 +37,11 @@
.stars { .stars {
display: inline-block; display: inline-block;
color: #FFA15E; color: #ffa15e;
font-size: $font-xlarge; font-size: $font-xlarge;
& > span { & > span {
color: #65DAFF; color: #65daff;
} }
} }

View file

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

View file

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

View file

@ -43,7 +43,6 @@
z-index: 2; z-index: 2;
} }
.CharacterImage { .CharacterImage {
aspect-ratio: 131 / 273; aspect-ratio: 131 / 273;
background: white; background: white;

View file

@ -1,85 +1,87 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import classnames from "classnames" import classnames from "classnames";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import CharacterHovercard from "~components/CharacterHovercard" import CharacterHovercard from "~components/CharacterHovercard";
import SearchModal from "~components/SearchModal" import SearchModal from "~components/SearchModal";
import UncapIndicator from "~components/UncapIndicator" import UncapIndicator from "~components/UncapIndicator";
import PlusIcon from "~public/icons/Add.svg" import PlusIcon from "~public/icons/Add.svg";
import type { SearchableObject } from "~types" import type { SearchableObject } from "~types";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
gridCharacter?: GridCharacter gridCharacter?: GridCharacter;
position: number position: number;
editable: boolean editable: boolean;
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void;
} }
const CharacterUnit = (props: Props) => { const CharacterUnit = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation("common");
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState);
const router = useRouter() const router = useRouter();
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const [imageUrl, setImageUrl] = useState("") const [imageUrl, setImageUrl] = useState("");
const classes = classnames({ const classes = classnames({
CharacterUnit: true, CharacterUnit: true,
editable: props.editable, editable: props.editable,
filled: props.gridCharacter !== undefined, filled: props.gridCharacter !== undefined,
}) });
const gridCharacter = props.gridCharacter const gridCharacter = props.gridCharacter;
const character = gridCharacter?.object const character = gridCharacter?.object;
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl();
}) });
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = "";
if (props.gridCharacter) { if (props.gridCharacter) {
const character = props.gridCharacter.object! const character = props.gridCharacter.object!;
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = "01" let suffix = "01";
if (props.gridCharacter.uncap_level == 6) suffix = "04" if (props.gridCharacter.uncap_level == 6) suffix = "04";
else if (props.gridCharacter.uncap_level == 5) suffix = "03" else if (props.gridCharacter.uncap_level == 5) suffix = "03";
else if (props.gridCharacter.uncap_level > 2) suffix = "02" else if (props.gridCharacter.uncap_level > 2) suffix = "02";
// Special casing for Lyria (and Young Cat eventually) // Special casing for Lyria (and Young Cat eventually)
if (props.gridCharacter.object.granblue_id === "3030182000") { if (props.gridCharacter.object.granblue_id === "3030182000") {
let element = 1 let element = 1;
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) { if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
element = grid.weapons.mainWeapon.element element = grid.weapons.mainWeapon.element;
} else if (party.element != 0) { } 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) { function passUncapData(uncap: number) {
if (props.gridCharacter) if (props.gridCharacter)
props.updateUncap(props.gridCharacter.id, props.position, uncap) props.updateUncap(props.gridCharacter.id, props.position, uncap);
} }
const image = ( const image = (
@ -93,7 +95,7 @@ const CharacterUnit = (props: Props) => {
"" ""
)} )}
</div> </div>
) );
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
@ -104,7 +106,7 @@ const CharacterUnit = (props: Props) => {
> >
{image} {image}
</SearchModal> </SearchModal>
) );
const unitContent = ( const unitContent = (
<div className={classes}> <div className={classes}>
@ -123,15 +125,15 @@ const CharacterUnit = (props: Props) => {
)} )}
<h3 className="CharacterName">{character?.name[locale]}</h3> <h3 className="CharacterName">{character?.name[locale]}</h3>
</div> </div>
) );
const withHovercard = ( const withHovercard = (
<CharacterHovercard gridCharacter={gridCharacter!}> <CharacterHovercard gridCharacter={gridCharacter!}>
{unitContent} {unitContent}
</CharacterHovercard> </CharacterHovercard>
) );
return gridCharacter && !props.editable ? withHovercard : unitContent return gridCharacter && !props.editable ? withHovercard : unitContent;
} };
export default CharacterUnit export default CharacterUnit;

View file

@ -26,8 +26,9 @@
cursor: pointer; cursor: pointer;
} }
&:hover, &[data-state="on"] { &:hover,
background:$grey-80; &[data-state="on"] {
background: $grey-80;
color: $grey-00; color: $grey-00;
&.fire { &.fire {

View file

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

View file

@ -1,5 +1,5 @@
#ExtraSummons { #ExtraSummons {
background: #FFEBD9; background: #ffebd9;
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@ -17,7 +17,7 @@
} }
& > span { & > span {
color: #825B39; color: #825b39;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -1,24 +1,24 @@
import React from "react" import React from "react";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import SummonUnit from "~components/SummonUnit" import SummonUnit from "~components/SummonUnit";
import { SearchableObject } from "~types" import { SearchableObject } from "~types";
import "./index.scss" import "./index.scss";
// Props // Props
interface Props { interface Props {
grid: GridArray<GridSummon> grid: GridArray<GridSummon>;
editable: boolean editable: boolean;
exists: boolean exists: boolean;
found?: boolean found?: boolean;
offset: number offset: number;
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void;
} }
const ExtraSummons = (props: Props) => { const ExtraSummons = (props: Props) => {
const numSummons: number = 2 const numSummons: number = 2;
const { t } = useTranslation("common") const { t } = useTranslation("common");
return ( return (
<div id="ExtraSummons"> <div id="ExtraSummons">
@ -36,11 +36,11 @@ const ExtraSummons = (props: Props) => {
updateUncap={props.updateUncap} updateUncap={props.updateUncap}
/> />
</li> </li>
) );
})} })}
</ul> </ul>
</div> </div>
) );
} };
export default ExtraSummons export default ExtraSummons;

View file

@ -1,5 +1,5 @@
#ExtraGrid { #ExtraGrid {
background: #ECEBFF; background: #ecebff;
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@ -17,7 +17,7 @@
} }
& > span { & > span {
color: #4F3C79; color: #4f3c79;
display: flex; display: flex;
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
@ -38,10 +38,10 @@
} }
.WeaponUnit .WeaponImage { .WeaponUnit .WeaponImage {
background: #D5D3F6; background: #d5d3f6;
} }
.WeaponUnit .WeaponImage .icon svg { .WeaponUnit .WeaponImage .icon svg {
fill: #8F8AC6; fill: #8f8ac6;
} }
} }

View file

@ -1,24 +1,24 @@
import React from "react" import React from "react";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import WeaponUnit from "~components/WeaponUnit" import WeaponUnit from "~components/WeaponUnit";
import type { SearchableObject } from "~types" import type { SearchableObject } from "~types";
import "./index.scss" import "./index.scss";
// Props // Props
interface Props { interface Props {
grid: GridArray<GridWeapon> grid: GridArray<GridWeapon>;
editable: boolean editable: boolean;
found?: boolean found?: boolean;
offset: number offset: number;
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void;
} }
const ExtraWeapons = (props: Props) => { const ExtraWeapons = (props: Props) => {
const numWeapons: number = 3 const numWeapons: number = 3;
const { t } = useTranslation("common") const { t } = useTranslation("common");
return ( return (
<div id="ExtraGrid"> <div id="ExtraGrid">
@ -36,11 +36,11 @@ const ExtraWeapons = (props: Props) => {
updateUncap={props.updateUncap} updateUncap={props.updateUncap}
/> />
</li> </li>
) );
})} })}
</ul> </ul>
</div> </div>
) );
} };
export default ExtraWeapons export default ExtraWeapons;

View file

@ -27,7 +27,8 @@
} }
} }
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ ::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: #a9a9a9 !important; color: #a9a9a9 !important;
opacity: 1; /* Firefox */ opacity: 1; /* Firefox */
} }

View file

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

View file

@ -26,7 +26,7 @@
} }
select { select {
background: url('/icons/Arrow.svg'), $grey-90; background: url("/icons/Arrow.svg"), $grey-90;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position-y: center; background-position-y: center;
background-position-x: 95%; background-position-x: 95%;
@ -37,7 +37,6 @@
max-width: 200px; max-width: 200px;
} }
.UserInfo { .UserInfo {
align-items: center; align-items: center;
display: flex; display: flex;
@ -52,11 +51,11 @@
width: $diameter; width: $diameter;
&.gran { &.gran {
background-color: #CEE7FE; background-color: #cee7fe;
} }
&.djeeta { &.djeeta {
background-color: #FFE1FE; background-color: #ffe1fe;
} }
} }
} }

View file

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

View file

@ -8,7 +8,8 @@
&:hover { &:hover {
background: white; background: white;
h2, .Grid { h2,
.Grid {
cursor: pointer; cursor: pointer;
} }
@ -105,12 +106,15 @@
flex-direction: row; flex-direction: row;
} }
.raid, .user, time { .raid,
.user,
time {
color: $grey-50; color: $grey-50;
font-size: $font-small; font-size: $font-small;
} }
.raid, .user { .raid,
.user {
flex-grow: 1; flex-grow: 1;
} }
@ -123,8 +127,8 @@
gap: calc($unit / 2); gap: calc($unit / 2);
align-items: center; align-items: center;
img,
img, .no-user { .no-user {
$diameter: 18px; $diameter: 18px;
border-radius: calc($diameter / 2); border-radius: calc($diameter / 2);
@ -133,11 +137,11 @@
} }
img.gran { img.gran {
background-color: #CEE7FE; background-color: #cee7fe;
} }
img.djeeta { img.djeeta {
background-color: #FFE1FE; background-color: #ffe1fe;
} }
.no-user { .no-user {

View file

@ -1,87 +1,89 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import classNames from "classnames" import classNames from "classnames";
import { accountState } from "~utils/accountState" import { accountState } from "~utils/accountState";
import { formatTimeAgo } from "~utils/timeAgo" import { formatTimeAgo } from "~utils/timeAgo";
import Button from "~components/Button" import Button from "~components/Button";
import { ButtonType } from "~utils/enums" import { ButtonType } from "~utils/enums";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
shortcode: string shortcode: string;
id: string id: string;
name: string name: string;
raid: Raid raid: Raid;
grid: GridWeapon[] grid: GridWeapon[];
user?: User user?: User;
favorited: boolean favorited: boolean;
createdAt: Date createdAt: Date;
displayUser?: boolean | false displayUser?: boolean | false;
onClick: (shortcode: string) => void onClick: (shortcode: string) => void;
onSave?: (partyId: string, favorited: boolean) => void onSave?: (partyId: string, favorited: boolean) => void;
} }
const GridRep = (props: Props) => { const GridRep = (props: Props) => {
const numWeapons: number = 9 const numWeapons: number = 9;
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState);
const router = useRouter() const router = useRouter();
const { t } = useTranslation("common") const { t } = useTranslation("common");
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const [mainhand, setMainhand] = useState<Weapon>() const [mainhand, setMainhand] = useState<Weapon>();
const [weapons, setWeapons] = useState<GridArray<Weapon>>({}) const [weapons, setWeapons] = useState<GridArray<Weapon>>({});
const [grid, setGrid] = useState<GridArray<GridWeapon>>({}) const [grid, setGrid] = useState<GridArray<GridWeapon>>({});
const titleClass = classNames({ const titleClass = classNames({
empty: !props.name, empty: !props.name,
}) });
const raidClass = classNames({ const raidClass = classNames({
raid: true, raid: true,
empty: !props.raid, empty: !props.raid,
}) });
const userClass = classNames({ const userClass = classNames({
user: true, user: true,
empty: !props.user, empty: !props.user,
}) });
useEffect(() => { useEffect(() => {
const newWeapons = Array(numWeapons) const newWeapons = Array(numWeapons);
const gridWeapons = Array(numWeapons) const gridWeapons = Array(numWeapons);
for (const [key, value] of Object.entries(props.grid)) { 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) { else if (!value.mainhand && value.position != null) {
newWeapons[value.position] = value.object newWeapons[value.position] = value.object;
gridWeapons[value.position] = value gridWeapons[value.position] = value;
} }
} }
setWeapons(newWeapons) setWeapons(newWeapons);
setGrid(gridWeapons) setGrid(gridWeapons);
}, [props.grid]) }, [props.grid]);
function navigate() { function navigate() {
props.onClick(props.shortcode) props.onClick(props.shortcode);
} }
function generateMainhandImage() { function generateMainhandImage() {
let url = "" let url = "";
if (mainhand) { if (mainhand) {
if (mainhand.element == 0 && props.grid[0].element) { if (mainhand.element == 0 && props.grid[0].element) {
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 { } 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`;
} }
} }
@ -89,20 +91,20 @@ const GridRep = (props: Props) => {
<img alt={mainhand.name[locale]} src={url} /> <img alt={mainhand.name[locale]} src={url} />
) : ( ) : (
"" ""
) );
} }
function generateGridImage(position: number) { function generateGridImage(position: number) {
let url = "" let url = "";
const weapon = weapons[position] const weapon = weapons[position];
const gridWeapon = grid[position] const gridWeapon = grid[position];
if (weapon && gridWeapon) { if (weapon && gridWeapon) {
if (weapon.element == 0 && gridWeapon.element) { 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 { } 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`;
} }
} }
@ -110,11 +112,11 @@ const GridRep = (props: Props) => {
<img alt={weapons[position]?.name[locale]} src={url} /> <img alt={weapons[position]?.name[locale]} src={url} />
) : ( ) : (
"" ""
) );
} }
function sendSaveData() { function sendSaveData() {
if (props.onSave) props.onSave(props.id, props.favorited) if (props.onSave) props.onSave(props.id, props.favorited);
} }
const userImage = () => { const userImage = () => {
@ -127,9 +129,9 @@ const GridRep = (props: Props) => {
/profile/${props.user.picture.picture}@2x.png 2x`} /profile/${props.user.picture.picture}@2x.png 2x`}
src={`/profile/${props.user.picture.picture}.png`} src={`/profile/${props.user.picture.picture}.png`}
/> />
) );
} else return <div className="no-user" /> } else return <div className="no-user" />;
} };
const details = ( const details = (
<div className="Details"> <div className="Details">
@ -145,7 +147,7 @@ const GridRep = (props: Props) => {
</time> </time>
</div> </div>
</div> </div>
) );
const detailsWithUsername = ( const detailsWithUsername = (
<div className="Details"> <div className="Details">
@ -181,7 +183,7 @@ const GridRep = (props: Props) => {
</time> </time>
</div> </div>
</div> </div>
) );
return ( return (
<div className="GridRep"> <div className="GridRep">
@ -198,12 +200,12 @@ const GridRep = (props: Props) => {
> >
{generateGridImage(i)} {generateGridImage(i)}
</li> </li>
) );
})} })}
</ul> </ul>
</div> </div>
</div> </div>
) );
} };
export default GridRep export default GridRep;

View file

@ -1,18 +1,18 @@
import classNames from "classnames" import classNames from "classnames";
import React from "react" import React from "react";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode;
} }
const GridRepCollection = (props: Props) => { const GridRepCollection = (props: Props) => {
const classes = classNames({ const classes = classNames({
GridRepCollection: true, 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,21 +1,21 @@
import React from 'react' import React from "react";
import './index.scss' import "./index.scss";
interface Props { interface Props {
position: 'top' | 'bottom' position: "top" | "bottom";
left: JSX.Element, left: JSX.Element;
right: JSX.Element right: JSX.Element;
} }
const Header = (props: Props) => { const Header = (props: Props) => {
return ( return (
<nav className={`Header ${props.position}`}> <nav className={`Header ${props.position}`}>
<div id="left">{ props.left }</div> <div id="left">{props.left}</div>
<div className="push" /> <div className="push" />
<div id="right">{ props.right }</div> <div id="right">{props.right}</div>
</nav> </nav>
) );
} };
export default Header export default Header;

View file

@ -73,7 +73,8 @@
} }
} }
.left, .right { .left,
.right {
color: white; color: white;
font-size: 10px; font-size: 10px;
font-weight: $bold; font-weight: $bold;
@ -97,7 +98,8 @@
color: $grey-40; color: $grey-40;
} }
& > a, & > span { & > a,
& > span {
display: block; display: block;
padding: 12px 12px; padding: 12px 12px;
} }

View file

@ -1,51 +1,51 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { getCookie, setCookie } from "cookies-next" import { getCookie, setCookie } from "cookies-next";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import Link from "next/link" import Link from "next/link";
import * as Switch from "@radix-ui/react-switch" import * as Switch from "@radix-ui/react-switch";
import AboutModal from "~components/AboutModal" import AboutModal from "~components/AboutModal";
import AccountModal from "~components/AccountModal" import AccountModal from "~components/AccountModal";
import LoginModal from "~components/LoginModal" import LoginModal from "~components/LoginModal";
import SignupModal from "~components/SignupModal" import SignupModal from "~components/SignupModal";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
authenticated: boolean authenticated: boolean;
username?: string username?: string;
logout?: () => void logout?: () => void;
} }
const HeaderMenu = (props: Props) => { const HeaderMenu = (props: Props) => {
const router = useRouter() const router = useRouter();
const { t } = useTranslation("common") const { t } = useTranslation("common");
const accountCookie = getCookie("account") const accountCookie = getCookie("account");
const accountData: AccountCookie = accountCookie const accountData: AccountCookie = accountCookie
? JSON.parse(accountCookie as string) ? JSON.parse(accountCookie as string)
: null : null;
const userCookie = getCookie("user") const userCookie = getCookie("user");
const userData: UserCookie = userCookie const userData: UserCookie = userCookie
? JSON.parse(userCookie as string) ? JSON.parse(userCookie as string)
: null : null;
const localeCookie = getCookie("NEXT_LOCALE") const localeCookie = getCookie("NEXT_LOCALE");
const [checked, setChecked] = useState(false) const [checked, setChecked] = useState(false);
useEffect(() => { useEffect(() => {
const locale = localeCookie const locale = localeCookie;
setChecked(locale === "ja" ? true : false) setChecked(locale === "ja" ? true : false);
}, [localeCookie]) }, [localeCookie]);
function handleCheckedChange(value: boolean) { function handleCheckedChange(value: boolean) {
const language = value ? "ja" : "en" const language = value ? "ja" : "en";
setCookie("NEXT_LOCALE", language, { path: "/" }) setCookie("NEXT_LOCALE", language, { path: "/" });
router.push(router.asPath, undefined, { locale: language }) router.push(router.asPath, undefined, { locale: language });
} }
function authItems() { function authItems() {
@ -92,7 +92,7 @@ const HeaderMenu = (props: Props) => {
</div> </div>
</ul> </ul>
</nav> </nav>
) );
} }
function unauthItems() { function unauthItems() {
@ -132,10 +132,10 @@ const HeaderMenu = (props: Props) => {
<SignupModal /> <SignupModal />
</div> </div>
</ul> </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 React, { useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import { jobGroups } from "~utils/jobGroups" import { jobGroups } from "~utils/jobGroups";
import "./index.scss" import "./index.scss";
// Props // Props
interface Props { interface Props {
currentJob?: string currentJob?: string;
onChange?: (job?: Job) => void onChange?: (job?: Job) => void;
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
} }
type GroupedJob = { [key: string]: Job[] } type GroupedJob = { [key: string]: Job[] };
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>( const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) { function useFieldSet(props, ref) {
// Set up router for locale // Set up router for locale
const router = useRouter() const router = useRouter();
const locale = router.locale || "en" const locale = router.locale || "en";
// Create snapshot of app state // Create snapshot of app state
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState);
// Set up local states for storing jobs // Set up local states for storing jobs
const [currentJob, setCurrentJob] = useState<Job>() const [currentJob, setCurrentJob] = useState<Job>();
const [jobs, setJobs] = useState<Job[]>() const [jobs, setJobs] = useState<Job[]>();
const [sortedJobs, setSortedJobs] = useState<GroupedJob>() const [sortedJobs, setSortedJobs] = useState<GroupedJob>();
// Set current job from state on mount // Set current job from state on mount
useEffect(() => { useEffect(() => {
setCurrentJob(party.job) setCurrentJob(party.job);
}, []) }, []);
// Organize jobs into groups on mount // Organize jobs into groups on mount
useEffect(() => { useEffect(() => {
const jobGroups = appState.jobs const jobGroups = appState.jobs
.map((job) => job.row) .map((job) => job.row)
.filter((value, index, self) => self.indexOf(value) === index) .filter((value, index, self) => self.indexOf(value) === index);
let groupedJobs: GroupedJob = {} let groupedJobs: GroupedJob = {};
jobGroups.forEach((group) => { jobGroups.forEach((group) => {
groupedJobs[group] = appState.jobs.filter((job) => job.row === group) groupedJobs[group] = appState.jobs.filter((job) => job.row === group);
}) });
setJobs(appState.jobs) setJobs(appState.jobs);
setSortedJobs(groupedJobs) setSortedJobs(groupedJobs);
}, [appState]) }, [appState]);
// Set current job on mount // Set current job on mount
useEffect(() => { useEffect(() => {
if (jobs && props.currentJob) { if (jobs && props.currentJob) {
const job = appState.jobs.find((job) => job.id === props.currentJob) const job = appState.jobs.find((job) => job.id === props.currentJob);
setCurrentJob(job) setCurrentJob(job);
} }
}, [appState, props.currentJob]) }, [appState, props.currentJob]);
// Enable changing select value // Enable changing select value
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (jobs) { if (jobs) {
const job = jobs.find((job) => job.id === event.target.value) const job = jobs.find((job) => job.id === event.target.value);
if (props.onChange) props.onChange(job) if (props.onChange) props.onChange(job);
setCurrentJob(job) setCurrentJob(job);
} }
} }
@ -79,16 +79,16 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
<option key={i} value={item.id}> <option key={i} value={item.id}>
{item.name[locale]} {item.name[locale]}
</option> </option>
) );
}) });
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale] const groupName = jobGroups.find((g) => g.slug === group)?.name[locale];
return ( return (
<optgroup key={group} label={groupName}> <optgroup key={group} label={groupName}>
{options} {options}
</optgroup> </optgroup>
) );
} }
return ( return (
@ -106,8 +106,8 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
? Object.keys(sortedJobs).map((x) => renderJobGroup(x)) ? Object.keys(sortedJobs).map((x) => renderJobGroup(x))
: ""} : ""}
</select> </select>
) );
} }
) );
export default JobDropdown export default JobDropdown;

View file

@ -1,99 +1,101 @@
import React, { ForwardedRef, useEffect, useState } from "react" import React, { ForwardedRef, useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import JobDropdown from "~components/JobDropdown" import JobDropdown from "~components/JobDropdown";
import JobSkillItem from "~components/JobSkillItem" import JobSkillItem from "~components/JobSkillItem";
import SearchModal from "~components/SearchModal" import SearchModal from "~components/SearchModal";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import type { JobSkillObject, SearchableObject } from "~types" import type { JobSkillObject, SearchableObject } from "~types";
import "./index.scss" import "./index.scss";
// Props // Props
interface Props { interface Props {
job?: Job job?: Job;
jobSkills: JobSkillObject jobSkills: JobSkillObject;
editable: boolean editable: boolean;
saveJob: (job: Job) => void saveJob: (job: Job) => void;
saveSkill: (skill: JobSkill, position: number) => void saveSkill: (skill: JobSkill, position: number) => void;
} }
const JobSection = (props: Props) => { const JobSection = (props: Props) => {
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState);
const { t } = useTranslation("common") const { t } = useTranslation("common");
const router = useRouter() const router = useRouter();
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const [job, setJob] = useState<Job>() const [job, setJob] = useState<Job>();
const [imageUrl, setImageUrl] = useState("") const [imageUrl, setImageUrl] = useState("");
const [numSkills, setNumSkills] = useState(4) const [numSkills, setNumSkills] = useState(4);
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>( const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
[] []
) );
const selectRef = React.createRef<HTMLSelectElement>() const selectRef = React.createRef<HTMLSelectElement>();
useEffect(() => { useEffect(() => {
// Set current job based on ID // Set current job based on ID
if (props.job) { if (props.job) {
setJob(props.job) setJob(props.job);
setSkills({ setSkills({
0: props.jobSkills[0], 0: props.jobSkills[0],
1: props.jobSkills[1], 1: props.jobSkills[1],
2: props.jobSkills[2], 2: props.jobSkills[2],
3: props.jobSkills[3], 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(() => { useEffect(() => {
generateImageUrl() generateImageUrl();
}) });
useEffect(() => { useEffect(() => {
if (job) { if (job) {
if ((party.job && job.id != party.job.id) || !party.job) if ((party.job && job.id != party.job.id) || !party.job)
appState.party.job = job appState.party.job = job;
if (job.row === "1") setNumSkills(3) if (job.row === "1") setNumSkills(3);
else setNumSkills(4) else setNumSkills(4);
} }
}, [job]) }, [job]);
function receiveJob(job?: Job) { function receiveJob(job?: Job) {
if (job) { if (job) {
setJob(job) setJob(job);
props.saveJob(job) props.saveJob(job);
} }
} }
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = "";
if (job) { if (job) {
const slug = job?.name.en.replaceAll(" ", "-").toLowerCase() const slug = job?.name.en.replaceAll(" ", "-").toLowerCase();
const gender = party.user && party.user.gender == 1 ? "b" : "a" const gender = party.user && party.user.gender == 1 ? "b" : "a";
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`;
} }
setImageUrl(imgSrc) setImageUrl(imgSrc);
} }
const canEditSkill = (skill?: JobSkill) => { const canEditSkill = (skill?: JobSkill) => {
if (job && skill) { 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) => { const skillItem = (index: number, editable: boolean) => {
return ( return (
@ -103,8 +105,8 @@ const JobSection = (props: Props) => {
key={`skill-${index}`} key={`skill-${index}`}
hasJob={job != undefined && job.id != "-1"} hasJob={job != undefined && job.id != "-1"}
/> />
) );
} };
const editableSkillItem = (index: number) => { const editableSkillItem = (index: number) => {
return ( return (
@ -117,17 +119,17 @@ const JobSection = (props: Props) => {
> >
{skillItem(index, true)} {skillItem(index, true)}
</SearchModal> </SearchModal>
) );
} };
function saveJobSkill(object: SearchableObject, position: number) { function saveJobSkill(object: SearchableObject, position: number) {
const skill = object as JobSkill const skill = object as JobSkill;
const newSkills = skills const newSkills = skills;
newSkills[position] = skill newSkills[position] = skill;
setSkills(newSkills) setSkills(newSkills);
props.saveSkill(skill, position) props.saveSkill(skill, position);
} }
// Render: JSX components // Render: JSX components
@ -159,7 +161,7 @@ const JobSection = (props: Props) => {
</ul> </ul>
</div> </div>
</section> </section>
) );
} };
export default JobSection export default JobSection;

View file

@ -1,40 +1,40 @@
import React from "react" import React from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import classNames from "classnames" import classNames from "classnames";
import PlusIcon from "~public/icons/Add.svg" import PlusIcon from "~public/icons/Add.svg";
import "./index.scss" import "./index.scss";
// Props // Props
interface Props extends React.ComponentPropsWithoutRef<"div"> { interface Props extends React.ComponentPropsWithoutRef<"div"> {
skill?: JobSkill skill?: JobSkill;
editable: boolean editable: boolean;
hasJob: boolean hasJob: boolean;
} }
const JobSkillItem = React.forwardRef<HTMLDivElement, Props>( const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
function useJobSkillItem({ ...props }, forwardedRef) { function useJobSkillItem({ ...props }, forwardedRef) {
const router = useRouter() const router = useRouter();
const { t } = useTranslation("common") const { t } = useTranslation("common");
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) router.locale && ["en", "ja"].includes(router.locale)
? router.locale ? router.locale
: "en" : "en";
const classes = classNames({ const classes = classNames({
JobSkill: true, JobSkill: true,
editable: props.editable, editable: props.editable,
}) });
const imageClasses = classNames({ const imageClasses = classNames({
placeholder: !props.skill, placeholder: !props.skill,
editable: props.editable && props.hasJob, editable: props.editable && props.hasJob,
}) });
const skillImage = () => { const skillImage = () => {
let jsx: React.ReactNode let jsx: React.ReactNode;
if (props.skill) { if (props.skill) {
jsx = ( jsx = (
@ -43,39 +43,39 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
className={imageClasses} className={imageClasses}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${props.skill.slug}.png`} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-skills/${props.skill.slug}.png`}
/> />
) );
} else { } else {
jsx = ( jsx = (
<div className={imageClasses}> <div className={imageClasses}>
{props.editable && props.hasJob ? <PlusIcon /> : ""} {props.editable && props.hasJob ? <PlusIcon /> : ""}
</div> </div>
) );
} }
return jsx return jsx;
} };
const label = () => { const label = () => {
let jsx: React.ReactNode let jsx: React.ReactNode;
if (props.skill) { if (props.skill) {
jsx = <p>{props.skill.name[locale]}</p> jsx = <p>{props.skill.name[locale]}</p>;
} else if (props.editable && props.hasJob) { } else if (props.editable && props.hasJob) {
jsx = <p className="placeholder">{t("job_skills.state.selectable")}</p> jsx = <p className="placeholder">{t("job_skills.state.selectable")}</p>;
} else { } else {
jsx = <p className="placeholder">{t("job_skills.state.no_skill")}</p> jsx = <p className="placeholder">{t("job_skills.state.no_skill")}</p>;
} }
return jsx return jsx;
} };
return ( return (
<div className={classes} onClick={props.onClick} ref={forwardedRef}> <div className={classes} onClick={props.onClick} ref={forwardedRef}>
{skillImage()} {skillImage()}
{label()} {label()}
</div> </div>
) );
} }
) );
export default JobSkillItem export default JobSkillItem;

View file

@ -1,29 +1,31 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { SkillGroup, skillClassification } from "~utils/skillGroups" import { SkillGroup, skillClassification } from "~utils/skillGroups";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
data: JobSkill data: JobSkill;
onClick: () => void onClick: () => void;
} }
const JobSkillResult = (props: Props) => { const JobSkillResult = (props: Props) => {
const router = useRouter() const router = useRouter();
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const skill = props.data const skill = props.data;
const [group, setGroup] = useState<SkillGroup | undefined>() const [group, setGroup] = useState<SkillGroup | undefined>();
useEffect(() => { useEffect(() => {
setGroup(skillClassification.find((group) => group.id === skill.color)) setGroup(skillClassification.find((group) => group.id === skill.color));
}, [skill, setGroup, skillClassification]) }, [skill, setGroup, skillClassification]);
const jobSkillUrl = () => 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 ( return (
<li className="JobSkillResult" onClick={props.onClick}> <li className="JobSkillResult" onClick={props.onClick}>
@ -35,7 +37,7 @@ const JobSkillResult = (props: Props) => {
</div> </div>
</div> </div>
</li> </li>
) );
} };
export default JobSkillResult export default JobSkillResult;

View file

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

View file

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

View file

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

View file

@ -1,55 +1,55 @@
import React, { useCallback, useEffect, useMemo, useState } from "react" import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import clonedeep from "lodash.clonedeep" import clonedeep from "lodash.clonedeep";
import PartySegmentedControl from "~components/PartySegmentedControl" import PartySegmentedControl from "~components/PartySegmentedControl";
import PartyDetails from "~components/PartyDetails" import PartyDetails from "~components/PartyDetails";
import WeaponGrid from "~components/WeaponGrid" import WeaponGrid from "~components/WeaponGrid";
import SummonGrid from "~components/SummonGrid" import SummonGrid from "~components/SummonGrid";
import CharacterGrid from "~components/CharacterGrid" import CharacterGrid from "~components/CharacterGrid";
import api from "~utils/api" import api from "~utils/api";
import { appState, initialAppState } from "~utils/appState" import { appState, initialAppState } from "~utils/appState";
import { GridType, TeamElement } from "~utils/enums" import { GridType, TeamElement } from "~utils/enums";
import "./index.scss" import "./index.scss";
// Props // Props
interface Props { interface Props {
new?: boolean new?: boolean;
team?: Party team?: Party;
raids: Raid[][] raids: Raid[][];
pushHistory?: (path: string) => void pushHistory?: (path: string) => void;
} }
const Party = (props: Props) => { const Party = (props: Props) => {
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie("account");
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null;
const headers = useMemo(() => { const headers = useMemo(() => {
return accountData return accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } } ? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {} : {};
}, [accountData]) }, [accountData]);
// Set up router // Set up router
const router = useRouter() const router = useRouter();
// Set up states // Set up states
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState);
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon) const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon);
// Reset state on first load // Reset state on first load
useEffect(() => { useEffect(() => {
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState);
appState.grid = resetState.grid appState.grid = resetState.grid;
if (props.team) storeParty(props.team) if (props.team) storeParty(props.team);
}, []) }, []);
// Methods: Creating a new party // Methods: Creating a new party
async function createParty(extra: boolean = false) { async function createParty(extra: boolean = false) {
@ -58,14 +58,14 @@ const Party = (props: Props) => {
...(accountData && { user_id: accountData.userId }), ...(accountData && { user_id: accountData.userId }),
extra: extra, extra: extra,
}, },
} };
return await api.endpoints.parties.create(body, headers) return await api.endpoints.parties.create(body, headers);
} }
// Methods: Updating the party's details // Methods: Updating the party's details
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) { function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
appState.party.extra = event.target.checked appState.party.extra = event.target.checked;
if (party.id) { if (party.id) {
api.endpoints.parties.update( api.endpoints.parties.update(
@ -74,7 +74,7 @@ const Party = (props: Props) => {
party: { extra: event.target.checked }, party: { extra: event.target.checked },
}, },
headers headers
) );
} }
} }
@ -98,11 +98,11 @@ const Party = (props: Props) => {
headers headers
) )
.then(() => { .then(() => {
appState.party.name = name appState.party.name = name;
appState.party.description = description appState.party.description = description;
appState.party.raid = raid appState.party.raid = raid;
appState.party.updated_at = party.updated_at appState.party.updated_at = party.updated_at;
}) });
} }
} }
@ -113,95 +113,95 @@ const Party = (props: Props) => {
.destroy({ id: appState.party.id, params: headers }) .destroy({ id: appState.party.id, params: headers })
.then(() => { .then(() => {
// Push to route // Push to route
router.push("/") router.push("/");
// Clean state // Clean state
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState);
Object.keys(resetState).forEach((key) => { Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key] appState[key] = resetState[key];
}) });
// Set party to be editable // Set party to be editable
appState.party.editable = true appState.party.editable = true;
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error);
}) });
} }
} }
// Methods: Storing party data // Methods: Storing party data
const storeParty = function (party: Party) { const storeParty = function (party: Party) {
// Store the important party and state-keeping values // Store the important party and state-keeping values
appState.party.name = party.name appState.party.name = party.name;
appState.party.description = party.description appState.party.description = party.description;
appState.party.raid = party.raid appState.party.raid = party.raid;
appState.party.updated_at = party.updated_at appState.party.updated_at = party.updated_at;
appState.party.job = party.job appState.party.job = party.job;
appState.party.jobSkills = party.job_skills appState.party.jobSkills = party.job_skills;
appState.party.id = party.id appState.party.id = party.id;
appState.party.extra = party.extra appState.party.extra = party.extra;
appState.party.user = party.user appState.party.user = party.user;
appState.party.favorited = party.favorited appState.party.favorited = party.favorited;
appState.party.created_at = party.created_at appState.party.created_at = party.created_at;
appState.party.updated_at = party.updated_at appState.party.updated_at = party.updated_at;
// Populate state // Populate state
storeCharacters(party.characters) storeCharacters(party.characters);
storeWeapons(party.weapons) storeWeapons(party.weapons);
storeSummons(party.summons) storeSummons(party.summons);
} };
const storeCharacters = (list: Array<GridCharacter>) => { const storeCharacters = (list: Array<GridCharacter>) => {
list.forEach((object: GridCharacter) => { list.forEach((object: GridCharacter) => {
if (object.position != null) if (object.position != null)
appState.grid.characters[object.position] = object appState.grid.characters[object.position] = object;
}) });
} };
const storeWeapons = (list: Array<GridWeapon>) => { const storeWeapons = (list: Array<GridWeapon>) => {
list.forEach((gridObject: GridWeapon) => { list.forEach((gridObject: GridWeapon) => {
if (gridObject.mainhand) { if (gridObject.mainhand) {
appState.grid.weapons.mainWeapon = gridObject appState.grid.weapons.mainWeapon = gridObject;
appState.party.element = gridObject.object.element appState.party.element = gridObject.object.element;
} else if (!gridObject.mainhand && gridObject.position != null) { } else if (!gridObject.mainhand && gridObject.position != null) {
appState.grid.weapons.allWeapons[gridObject.position] = gridObject appState.grid.weapons.allWeapons[gridObject.position] = gridObject;
}
})
} }
});
};
const storeSummons = (list: Array<GridSummon>) => { const storeSummons = (list: Array<GridSummon>) => {
list.forEach((gridObject: GridSummon) => { list.forEach((gridObject: GridSummon) => {
if (gridObject.main) appState.grid.summons.mainSummon = gridObject if (gridObject.main) appState.grid.summons.mainSummon = gridObject;
else if (gridObject.friend) else if (gridObject.friend)
appState.grid.summons.friendSummon = gridObject appState.grid.summons.friendSummon = gridObject;
else if ( else if (
!gridObject.main && !gridObject.main &&
!gridObject.friend && !gridObject.friend &&
gridObject.position != null gridObject.position != null
) )
appState.grid.summons.allSummons[gridObject.position] = gridObject appState.grid.summons.allSummons[gridObject.position] = gridObject;
}) });
} };
// Methods: Navigating with segmented control // Methods: Navigating with segmented control
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) { function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
switch (event.target.value) { switch (event.target.value) {
case "class": case "class":
setCurrentTab(GridType.Class) setCurrentTab(GridType.Class);
break break;
case "characters": case "characters":
setCurrentTab(GridType.Character) setCurrentTab(GridType.Character);
break break;
case "weapons": case "weapons":
setCurrentTab(GridType.Weapon) setCurrentTab(GridType.Weapon);
break break;
case "summons": case "summons":
setCurrentTab(GridType.Summon) setCurrentTab(GridType.Summon);
break break;
default: default:
break break;
} }
} }
@ -212,7 +212,7 @@ const Party = (props: Props) => {
onClick={segmentClicked} onClick={segmentClicked}
onCheckboxChange={checkboxChanged} onCheckboxChange={checkboxChanged}
/> />
) );
const weaponGrid = ( const weaponGrid = (
<WeaponGrid <WeaponGrid
@ -221,7 +221,7 @@ const Party = (props: Props) => {
createParty={createParty} createParty={createParty}
pushHistory={props.pushHistory} pushHistory={props.pushHistory}
/> />
) );
const summonGrid = ( const summonGrid = (
<SummonGrid <SummonGrid
@ -230,7 +230,7 @@ const Party = (props: Props) => {
createParty={createParty} createParty={createParty}
pushHistory={props.pushHistory} pushHistory={props.pushHistory}
/> />
) );
const characterGrid = ( const characterGrid = (
<CharacterGrid <CharacterGrid
@ -239,18 +239,18 @@ const Party = (props: Props) => {
createParty={createParty} createParty={createParty}
pushHistory={props.pushHistory} pushHistory={props.pushHistory}
/> />
) );
const currentGrid = () => { const currentGrid = () => {
switch (currentTab) { switch (currentTab) {
case GridType.Character: case GridType.Character:
return characterGrid return characterGrid;
case GridType.Weapon: case GridType.Weapon:
return weaponGrid return weaponGrid;
case GridType.Summon: case GridType.Summon:
return summonGrid return summonGrid;
}
} }
};
return ( return (
<div> <div>
@ -264,7 +264,7 @@ const Party = (props: Props) => {
/> />
} }
</div> </div>
) );
} };
export default Party export default Party;

View file

@ -10,9 +10,7 @@
top: $unit; top: $unit;
height: 0; height: 0;
z-index: 2; z-index: 2;
transition: opacity 0.2s ease-in-out, transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
top 0.2s ease-in-out;
&.Visible { &.Visible {
display: block; display: block;
@ -51,8 +49,7 @@
&.ReadOnly { &.ReadOnly {
top: $unit * -1; top: $unit * -1;
transition: opacity 0.2s ease-in-out, transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
top 0.2s ease-in-out;
&.Visible { &.Visible {
display: block; display: block;
@ -71,7 +68,6 @@
white-space: pre-line; white-space: pre-line;
} }
h1 { h1 {
font-size: $font-xlarge; font-size: $font-xlarge;
font-weight: $normal; font-weight: $normal;
@ -119,7 +115,8 @@
gap: calc($unit / 2); gap: calc($unit / 2);
margin-top: 1px; margin-top: 1px;
img, .no-user { img,
.no-user {
$diameter: 24px; $diameter: 24px;
border-radius: calc($diameter / 2); border-radius: calc($diameter / 2);
@ -128,11 +125,11 @@
} }
img.gran { img.gran {
background-color: #CEE7FE; background-color: #cee7fe;
} }
img.djeeta { img.djeeta {
background-color: #FFE1FE; background-color: #ffe1fe;
} }
.no-user { .no-user {

View file

@ -1,26 +1,26 @@
import React, { useState } from "react" import React, { useState } from "react";
import Head from "next/head" import Head from "next/head";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import Linkify from "react-linkify" import Linkify from "react-linkify";
import classNames from "classnames" import classNames from "classnames";
import * as AlertDialog from "@radix-ui/react-alert-dialog" import * as AlertDialog from "@radix-ui/react-alert-dialog";
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from "~public/icons/Cross.svg";
import Button from "~components/Button" import Button from "~components/Button";
import CharLimitedFieldset from "~components/CharLimitedFieldset" import CharLimitedFieldset from "~components/CharLimitedFieldset";
import RaidDropdown from "~components/RaidDropdown" import RaidDropdown from "~components/RaidDropdown";
import TextFieldset from "~components/TextFieldset" import TextFieldset from "~components/TextFieldset";
import { accountState } from "~utils/accountState" import { accountState } from "~utils/accountState";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import "./index.scss" import "./index.scss";
import Link from "next/link" import Link from "next/link";
import { formatTimeAgo } from "~utils/timeAgo" import { formatTimeAgo } from "~utils/timeAgo";
const emptyRaid: Raid = { const emptyRaid: Raid = {
id: "", id: "",
@ -32,50 +32,50 @@ const emptyRaid: Raid = {
level: 0, level: 0,
group: 0, group: 0,
element: 0, element: 0,
} };
// Props // Props
interface Props { interface Props {
editable: boolean editable: boolean;
updateCallback: (name?: string, description?: string, raid?: Raid) => void updateCallback: (name?: string, description?: string, raid?: Raid) => void;
deleteCallback: ( deleteCallback: (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => void ) => void;
} }
const PartyDetails = (props: Props) => { const PartyDetails = (props: Props) => {
const { party, raids } = useSnapshot(appState) const { party, raids } = useSnapshot(appState);
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState);
const { t } = useTranslation("common") const { t } = useTranslation("common");
const router = useRouter() const router = useRouter();
const locale = router.locale || "en" const locale = router.locale || "en";
const nameInput = React.createRef<HTMLInputElement>() const nameInput = React.createRef<HTMLInputElement>();
const descriptionInput = React.createRef<HTMLTextAreaElement>() const descriptionInput = React.createRef<HTMLTextAreaElement>();
const raidSelect = React.createRef<HTMLSelectElement>() const raidSelect = React.createRef<HTMLSelectElement>();
const readOnlyClasses = classNames({ const readOnlyClasses = classNames({
PartyDetails: true, PartyDetails: true,
ReadOnly: true, ReadOnly: true,
Visible: !party.detailsVisible, Visible: !party.detailsVisible,
}) });
const editableClasses = classNames({ const editableClasses = classNames({
PartyDetails: true, PartyDetails: true,
Editable: true, Editable: true,
Visible: party.detailsVisible, Visible: party.detailsVisible,
}) });
const emptyClasses = classNames({ const emptyClasses = classNames({
EmptyDetails: true, EmptyDetails: true,
Visible: !party.detailsVisible, Visible: !party.detailsVisible,
}) });
const userClass = classNames({ const userClass = classNames({
user: true, user: true,
empty: !party.user, empty: !party.user,
}) });
const linkClass = classNames({ const linkClass = classNames({
wind: party && party.element == 1, wind: party && party.element == 1,
@ -84,42 +84,42 @@ const PartyDetails = (props: Props) => {
earth: party && party.element == 4, earth: party && party.element == 4,
dark: party && party.element == 5, dark: party && party.element == 5,
light: party && party.element == 6, light: party && party.element == 6,
}) });
const [errors, setErrors] = useState<{ [key: string]: string }>({ const [errors, setErrors] = useState<{ [key: string]: string }>({
name: "", name: "",
description: "", description: "",
}) });
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) { function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault() event.preventDefault();
const { name, value } = event.target const { name, value } = event.target;
let newErrors = errors let newErrors = errors;
setErrors(newErrors) setErrors(newErrors);
} }
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) { function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
event.preventDefault() event.preventDefault();
const { name, value } = event.target const { name, value } = event.target;
let newErrors = errors let newErrors = errors;
setErrors(newErrors) setErrors(newErrors);
} }
function toggleDetails() { function toggleDetails() {
appState.party.detailsVisible = !appState.party.detailsVisible appState.party.detailsVisible = !appState.party.detailsVisible;
} }
function updateDetails(event: React.MouseEvent) { function updateDetails(event: React.MouseEvent) {
const nameValue = nameInput.current?.value const nameValue = nameInput.current?.value;
const descriptionValue = descriptionInput.current?.value const descriptionValue = descriptionInput.current?.value;
const raid = raids.find((raid) => raid.slug === raidSelect.current?.value) const raid = raids.find((raid) => raid.slug === raidSelect.current?.value);
props.updateCallback(nameValue, descriptionValue, raid) props.updateCallback(nameValue, descriptionValue, raid);
toggleDetails() toggleDetails();
} }
const userImage = () => { const userImage = () => {
@ -132,9 +132,9 @@ const PartyDetails = (props: Props) => {
/profile/${party.user.picture.picture}@2x.png 2x`} /profile/${party.user.picture.picture}@2x.png 2x`}
src={`/profile/${party.user.picture.picture}.png`} src={`/profile/${party.user.picture.picture}.png`}
/> />
) );
else return <div className="no-user" /> else return <div className="no-user" />;
} };
const userBlock = () => { const userBlock = () => {
return ( return (
@ -142,8 +142,8 @@ const PartyDetails = (props: Props) => {
{userImage()} {userImage()}
{party.user ? party.user.username : t("no_user")} {party.user ? party.user.username : t("no_user")}
</div> </div>
) );
} };
const linkedUserBlock = (user: User) => { const linkedUserBlock = (user: User) => {
return ( return (
@ -152,8 +152,8 @@ const PartyDetails = (props: Props) => {
<a className={linkClass}>{userBlock()}</a> <a className={linkClass}>{userBlock()}</a>
</Link> </Link>
</div> </div>
) );
} };
const linkedRaidBlock = (raid: Raid) => { const linkedRaidBlock = (raid: Raid) => {
return ( return (
@ -162,8 +162,8 @@ const PartyDetails = (props: Props) => {
<a className={`Raid ${linkClass}`}>{raid.name[locale]}</a> <a className={`Raid ${linkClass}`}>{raid.name[locale]}</a>
</Link> </Link>
</div> </div>
) );
} };
const deleteButton = () => { const deleteButton = () => {
if (party.editable) { if (party.editable) {
@ -198,11 +198,11 @@ const PartyDetails = (props: Props) => {
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Portal> </AlertDialog.Portal>
</AlertDialog.Root> </AlertDialog.Root>
) );
} else { } else {
return "" return "";
}
} }
};
const editable = ( const editable = (
<section className={editableClasses}> <section className={editableClasses}>
@ -246,7 +246,7 @@ const PartyDetails = (props: Props) => {
</div> </div>
</div> </div>
</section> </section>
) );
const readOnly = ( const readOnly = (
<section className={readOnlyClasses}> <section className={readOnlyClasses}>
@ -286,7 +286,7 @@ const PartyDetails = (props: Props) => {
"" ""
)} )}
</section> </section>
) );
const emptyDetails = ( const emptyDetails = (
<div className={emptyClasses}> <div className={emptyClasses}>
@ -298,25 +298,28 @@ const PartyDetails = (props: Props) => {
<div /> <div />
)} )}
</div> </div>
) );
const generateTitle = () => { const generateTitle = () => {
let title = party.raid ? `[${party.raid?.name[locale]}] ` : "" let title = party.raid ? `[${party.raid?.name[locale]}] ` : "";
const username = const username =
party.user != null ? `@${party.user?.username}` : t("header.anonymous") party.user != null ? `@${party.user?.username}` : t("header.anonymous");
if (party.name != null) if (party.name != null)
title += t("header.byline", { partyName: party.name, username: username }) title += t("header.byline", {
partyName: party.name,
username: username,
});
else if (party.name == null && party.editable && router.route === "/new") else if (party.name == null && party.editable && router.route === "/new")
title = t("header.new_team") title = t("header.new_team");
else else
title += t("header.untitled_team", { title += t("header.untitled_team", {
username: username, username: username,
}) });
return title return title;
} };
return ( return (
<div> <div>
@ -344,7 +347,7 @@ const PartyDetails = (props: Props) => {
: emptyDetails} : emptyDetails}
{editable} {editable}
</div> </div>
) );
} };
export default PartyDetails export default PartyDetails;

View file

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

View file

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

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 ArrowIcon from "~public/icons/Arrow.svg";
import './index.scss' import "./index.scss";
interface Props { interface Props {
label: string label: string;
open: boolean open: boolean;
numSelected: number numSelected: number;
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void;
children: React.ReactNode children: React.ReactNode;
} }
const SearchFilter = (props: Props) => { const SearchFilter = (props: Props) => {
@ -28,7 +28,7 @@ const SearchFilter = (props: Props) => {
<DropdownMenu.Arrow /> <DropdownMenu.Arrow />
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
) );
} };
export default SearchFilter export default SearchFilter;

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

View file

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

View file

@ -1,18 +1,16 @@
import React from 'react' import React from "react";
import './index.scss' import "./index.scss";
interface Props { interface Props {
groupName: string groupName: string;
name: string name: string;
selected: boolean selected: boolean;
children: string children: string;
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void onClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }
const Segment: React.FC<Props> = (props: Props) => { const Segment: React.FC<Props> = (props: Props) => {
return ( return (
<div className="Segment"> <div className="Segment">
<input <input
@ -23,11 +21,9 @@ const Segment: React.FC<Props> = (props: Props) => {
checked={props.selected} checked={props.selected}
onChange={props.onClick} onChange={props.onClick}
/> />
<label htmlFor={props.name}> <label htmlFor={props.name}>{props.children}</label>
{props.children}
</label>
</div> </div>
) );
} };
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 { interface Props {
elementClass?: string elementClass?: string;
} }
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => { const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
return ( return (
<div className="SegmentedControlWrapper"> <div className="SegmentedControlWrapper">
<div className={`SegmentedControl ${(elementClass) ? elementClass : ''}`}> <div className={`SegmentedControl ${elementClass ? elementClass : ""}`}>
{children} {children}
</div> </div>
</div> </div>
) );
} };
export default SegmentedControl export default SegmentedControl;

View file

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

View file

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

View file

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

View file

@ -37,11 +37,11 @@
.stars { .stars {
display: inline-block; display: inline-block;
color: #FFA15E; color: #ffa15e;
font-size: $font-xlarge; font-size: $font-xlarge;
& > span { & > span {
color: #65DAFF; color: #65daff;
} }
} }

View file

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

View file

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

View file

@ -85,7 +85,8 @@
display: flex; display: flex;
} }
h3, ul { h3,
ul {
display: none; display: none;
} }

View file

@ -1,34 +1,36 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import classnames from "classnames" import classnames from "classnames";
import SearchModal from "~components/SearchModal" import SearchModal from "~components/SearchModal";
import SummonHovercard from "~components/SummonHovercard" import SummonHovercard from "~components/SummonHovercard";
import UncapIndicator from "~components/UncapIndicator" import UncapIndicator from "~components/UncapIndicator";
import PlusIcon from "~public/icons/Add.svg" import PlusIcon from "~public/icons/Add.svg";
import type { SearchableObject } from "~types" import type { SearchableObject } from "~types";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
gridSummon: GridSummon | undefined gridSummon: GridSummon | undefined;
unitType: 0 | 1 | 2 unitType: 0 | 1 | 2;
position: number position: number;
editable: boolean editable: boolean;
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void;
} }
const SummonUnit = (props: Props) => { const SummonUnit = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation("common");
const [imageUrl, setImageUrl] = useState("") const [imageUrl, setImageUrl] = useState("");
const router = useRouter() const router = useRouter();
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const classes = classnames({ const classes = classnames({
SummonUnit: true, SummonUnit: true,
@ -37,19 +39,19 @@ const SummonUnit = (props: Props) => {
friend: props.unitType == 2, friend: props.unitType == 2,
editable: props.editable, editable: props.editable,
filled: props.gridSummon !== undefined, filled: props.gridSummon !== undefined,
}) });
const gridSummon = props.gridSummon const gridSummon = props.gridSummon;
const summon = gridSummon?.object const summon = gridSummon?.object;
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl();
}) });
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = "";
if (props.gridSummon) { if (props.gridSummon) {
const summon = props.gridSummon.object! const summon = props.gridSummon.object!;
const upgradedSummons = [ const upgradedSummons = [
"2040094000", "2040094000",
@ -66,28 +68,28 @@ const SummonUnit = (props: Props) => {
"2040027000", "2040027000",
"2040046000", "2040046000",
"2040047000", "2040047000",
] ];
let suffix = "" let suffix = "";
if ( if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5 props.gridSummon.uncap_level == 5
) )
suffix = "_02" suffix = "_02";
// Generate the correct source for the summon // Generate the correct source for the summon
if (props.unitType == 0 || props.unitType == 2) if (props.unitType == 0 || props.unitType == 2)
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 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) { function passUncapData(uncap: number) {
if (props.gridSummon) if (props.gridSummon)
props.updateUncap(props.gridSummon.id, props.position, uncap) props.updateUncap(props.gridSummon.id, props.position, uncap);
} }
const image = ( const image = (
@ -101,7 +103,7 @@ const SummonUnit = (props: Props) => {
"" ""
)} )}
</div> </div>
) );
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
@ -112,7 +114,7 @@ const SummonUnit = (props: Props) => {
> >
{image} {image}
</SearchModal> </SearchModal>
) );
const unitContent = ( const unitContent = (
<div className={classes}> <div className={classes}>
@ -131,13 +133,13 @@ const SummonUnit = (props: Props) => {
)} )}
<h3 className="SummonName">{summon?.name[locale]}</h3> <h3 className="SummonName">{summon?.name[locale]}</h3>
</div> </div>
) );
const withHovercard = ( const withHovercard = (
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard> <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,5 +1,6 @@
.Fieldset textarea { .Fieldset textarea {
color: $grey-00; color: $grey-00;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: system-ui, -apple-system, "Helvetica Neue", Helvetica, Arial,
sans-serif;
line-height: 21px; line-height: 21px;
} }

View file

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

View file

@ -43,11 +43,11 @@
} }
&-checkbox:checked + &-label { &-checkbox:checked + &-label {
background: #ECEBFF; background: #ecebff;
} }
&-checkbox:checked + &-label &-switch { &-checkbox:checked + &-label &-switch {
background: #8C86FF; background: #8c86ff;
} }
&-checkbox:checked + &-label { &-checkbox:checked + &-label {

View file

@ -1,12 +1,12 @@
import React from 'react' import React from "react";
import './index.scss' import "./index.scss";
interface Props { interface Props {
name: string name: string;
checked: boolean checked: boolean;
editable: boolean editable: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }
const ToggleSwitch: React.FC<Props> = (props: Props) => { const ToggleSwitch: React.FC<Props> = (props: Props) => {
@ -25,7 +25,7 @@ const ToggleSwitch: React.FC<Props> = (props: Props) => {
<span className="toggle-switch-switch" /> <span className="toggle-switch-switch" />
</label> </label>
</div> </div>
) );
} };
export default ToggleSwitch export default ToggleSwitch;

View file

@ -1,97 +1,97 @@
import React from "react" import React from "react";
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio";
import { getCookie, deleteCookie } from "cookies-next" import { getCookie, deleteCookie } from "cookies-next";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import clonedeep from "lodash.clonedeep" import clonedeep from "lodash.clonedeep";
import api from "~utils/api" import api from "~utils/api";
import { accountState, initialAccountState } from "~utils/accountState" import { accountState, initialAccountState } from "~utils/accountState";
import { appState, initialAppState } from "~utils/appState" import { appState, initialAppState } from "~utils/appState";
import Header from "~components/Header" import Header from "~components/Header";
import Button from "~components/Button" import Button from "~components/Button";
import HeaderMenu from "~components/HeaderMenu" import HeaderMenu from "~components/HeaderMenu";
const TopHeader = () => { const TopHeader = () => {
const { t } = useTranslation("common") const { t } = useTranslation("common");
// Cookies // Cookies
const accountCookie = getCookie("account") const accountCookie = getCookie("account");
const userCookie = getCookie("user") const userCookie = getCookie("user");
const headers = {} const headers = {};
// accountCookies.account != null // accountCookies.account != null
// ? { // ? {
// Authorization: `Bearer ${accountCookies.account.access_token}`, // Authorization: `Bearer ${accountCookies.account.access_token}`,
// } // }
// : {} // : {}
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState);
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState);
const router = useRouter() const router = useRouter();
function copyToClipboard() { function copyToClipboard() {
const el = document.createElement("input") const el = document.createElement("input");
el.value = window.location.href el.value = window.location.href;
el.id = "url-input" el.id = "url-input";
document.body.appendChild(el) document.body.appendChild(el);
el.select() el.select();
document.execCommand("copy") document.execCommand("copy");
el.remove() el.remove();
} }
function newParty() { function newParty() {
// Push the root URL // Push the root URL
router.push("/") router.push("/");
// Clean state // Clean state
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState);
Object.keys(resetState).forEach((key) => { Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key] appState[key] = resetState[key];
}) });
// Set party to be editable // Set party to be editable
appState.party.editable = true appState.party.editable = true;
} }
function logout() { function logout() {
deleteCookie("account") deleteCookie("account");
deleteCookie("user") deleteCookie("user");
// Clean state // Clean state
const resetState = clonedeep(initialAccountState) const resetState = clonedeep(initialAccountState);
Object.keys(resetState).forEach((key) => { Object.keys(resetState).forEach((key) => {
if (key !== "language") accountState[key] = resetState[key] if (key !== "language") accountState[key] = resetState[key];
}) });
if (router.route != "/new") appState.party.editable = false if (router.route != "/new") appState.party.editable = false;
router.push("/") router.push("/");
return false return false;
} }
function toggleFavorite() { function toggleFavorite() {
if (party.favorited) unsaveFavorite() if (party.favorited) unsaveFavorite();
else saveFavorite() else saveFavorite();
} }
function saveFavorite() { function saveFavorite() {
if (party.id) if (party.id)
api.saveTeam({ id: party.id, params: headers }).then((response) => { api.saveTeam({ id: party.id, params: headers }).then((response) => {
if (response.status == 201) appState.party.favorited = true if (response.status == 201) appState.party.favorited = true;
}) });
else console.error("Failed to save team: No party ID") else console.error("Failed to save team: No party ID");
} }
function unsaveFavorite() { function unsaveFavorite() {
if (party.id) if (party.id)
api.unsaveTeam({ id: party.id, params: headers }).then((response) => { api.unsaveTeam({ id: party.id, params: headers }).then((response) => {
if (response.status == 200) appState.party.favorited = false if (response.status == 200) appState.party.favorited = false;
}) });
else console.error("Failed to unsave team: No party ID") else console.error("Failed to unsave team: No party ID");
} }
const leftNav = () => { const leftNav = () => {
@ -108,8 +108,8 @@ const TopHeader = () => {
<HeaderMenu authenticated={account.authorized} /> <HeaderMenu authenticated={account.authorized} />
)} )}
</div> </div>
) );
} };
const saveButton = () => { const saveButton = () => {
if (party.favorited) if (party.favorited)
@ -117,14 +117,14 @@ const TopHeader = () => {
<Button icon="save" active={true} onClick={toggleFavorite}> <Button icon="save" active={true} onClick={toggleFavorite}>
Saved Saved
</Button> </Button>
) );
else else
return ( return (
<Button icon="save" onClick={toggleFavorite}> <Button icon="save" onClick={toggleFavorite}>
Save Save
</Button> </Button>
) );
} };
const rightNav = () => { const rightNav = () => {
return ( return (
@ -145,10 +145,10 @@ const TopHeader = () => {
{t("buttons.new")} {t("buttons.new")}
</Button> </Button>
</div> </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 React, { useEffect, useRef, useState } from "react";
import UncapStar from "~components/UncapStar" import UncapStar from "~components/UncapStar";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
type: "character" | "weapon" | "summon" type: "character" | "weapon" | "summon";
rarity?: number rarity?: number;
uncapLevel?: number uncapLevel?: number;
flb: boolean flb: boolean;
ulb: boolean ulb: boolean;
special: boolean special: boolean;
updateUncap?: (uncap: number) => void updateUncap?: (uncap: number) => void;
} }
const UncapIndicator = (props: Props) => { const UncapIndicator = (props: Props) => {
const [uncap, setUncap] = useState(props.uncapLevel) const [uncap, setUncap] = useState(props.uncapLevel);
const numStars = setNumStars() const numStars = setNumStars();
function setNumStars() { function setNumStars() {
let numStars let numStars;
if (props.type === "character") { if (props.type === "character") {
if (props.special) { if (props.special) {
if (props.ulb) { if (props.ulb) {
numStars = 5 numStars = 5;
} else if (props.flb) { } else if (props.flb) {
numStars = 4 numStars = 4;
} else { } else {
numStars = 3 numStars = 3;
} }
} else { } else {
if (props.ulb) { if (props.ulb) {
numStars = 6 numStars = 6;
} else if (props.flb) { } else if (props.flb) {
numStars = 5 numStars = 5;
} else { } else {
numStars = 4 numStars = 4;
} }
} }
} else { } else {
if (props.ulb) { if (props.ulb) {
numStars = 5 numStars = 5;
} else if (props.flb) { } else if (props.flb) {
numStars = 4 numStars = 4;
} else { } else {
numStars = 3 numStars = 3;
} }
} }
return numStars return numStars;
} }
function toggleStar(index: number, empty: boolean) { function toggleStar(index: number, empty: boolean) {
if (props.updateUncap) { if (props.updateUncap) {
if (empty) props.updateUncap(index + 1) if (empty) props.updateUncap(index + 1);
else props.updateUncap(index) else props.updateUncap(index);
} }
} }
@ -67,8 +67,8 @@ const UncapIndicator = (props: Props) => {
index={i} index={i}
onClick={toggleStar} onClick={toggleStar}
/> />
) );
} };
const ulb = (i: number) => { const ulb = (i: number) => {
return ( return (
@ -79,8 +79,8 @@ const UncapIndicator = (props: Props) => {
index={i} index={i}
onClick={toggleStar} onClick={toggleStar}
/> />
) );
} };
const flb = (i: number) => { const flb = (i: number) => {
return ( return (
@ -91,8 +91,8 @@ const UncapIndicator = (props: Props) => {
index={i} index={i}
onClick={toggleStar} onClick={toggleStar}
/> />
) );
} };
const mlb = (i: number) => { const mlb = (i: number) => {
// console.log("MLB; Number of stars:", props.uncapLevel) // console.log("MLB; Number of stars:", props.uncapLevel)
@ -103,27 +103,27 @@ const UncapIndicator = (props: Props) => {
index={i} index={i}
onClick={toggleStar} onClick={toggleStar}
/> />
) );
} };
return ( return (
<ul className="UncapIndicator"> <ul className="UncapIndicator">
{Array.from(Array(numStars)).map((x, i) => { {Array.from(Array(numStars)).map((x, i) => {
if (props.type === "character" && i > 4) { if (props.type === "character" && i > 4) {
if (props.special) return ulb(i) if (props.special) return ulb(i);
else return transcendence(i) else return transcendence(i);
} else if ( } else if (
(props.special && props.type === "character" && i == 3) || (props.special && props.type === "character" && i == 3) ||
(props.type === "character" && i == 4) || (props.type === "character" && i == 4) ||
(props.type !== "character" && i > 2) (props.type !== "character" && i > 2)
) { ) {
return flb(i) return flb(i);
} else { } else {
return mlb(i) return mlb(i);
} }
})} })}
</ul> </ul>
) );
} };
export default UncapIndicator export default UncapIndicator;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,69 +1,71 @@
import React, { useState } from "react" import React, { useState } from "react";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import { AxiosResponse } from "axios" import { AxiosResponse } from "axios";
import * as Dialog from "@radix-ui/react-dialog" import * as Dialog from "@radix-ui/react-dialog";
import AXSelect from "~components/AxSelect" import AXSelect from "~components/AxSelect";
import ElementToggle from "~components/ElementToggle" import ElementToggle from "~components/ElementToggle";
import WeaponKeyDropdown from "~components/WeaponKeyDropdown" import WeaponKeyDropdown from "~components/WeaponKeyDropdown";
import Button from "~components/Button" import Button from "~components/Button";
import api from "~utils/api" import api from "~utils/api";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import CrossIcon from "~public/icons/Cross.svg" import CrossIcon from "~public/icons/Cross.svg";
import "./index.scss" import "./index.scss";
interface GridWeaponObject { interface GridWeaponObject {
weapon: { weapon: {
element?: number element?: number;
weapon_key1_id?: string weapon_key1_id?: string;
weapon_key2_id?: string weapon_key2_id?: string;
weapon_key3_id?: string weapon_key3_id?: string;
ax_modifier1?: number ax_modifier1?: number;
ax_modifier2?: number ax_modifier2?: number;
ax_strength1?: number ax_strength1?: number;
ax_strength2?: number ax_strength2?: number;
} };
} }
interface Props { interface Props {
gridWeapon: GridWeapon gridWeapon: GridWeapon;
children: React.ReactNode children: React.ReactNode;
} }
const WeaponModal = (props: Props) => { const WeaponModal = (props: Props) => {
const router = useRouter() const router = useRouter();
const locale = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
const { t } = useTranslation("common") ? router.locale
: "en";
const { t } = useTranslation("common");
// Cookies // Cookies
const cookie = getCookie("account") const cookie = getCookie("account");
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null;
const headers = accountData const headers = accountData
? { Authorization: `Bearer ${accountData.token}` } ? { Authorization: `Bearer ${accountData.token}` }
: {} : {};
// Refs // Refs
const weaponKey1Select = React.createRef<HTMLSelectElement>() const weaponKey1Select = React.createRef<HTMLSelectElement>();
const weaponKey2Select = React.createRef<HTMLSelectElement>() const weaponKey2Select = React.createRef<HTMLSelectElement>();
const weaponKey3Select = React.createRef<HTMLSelectElement>() const weaponKey3Select = React.createRef<HTMLSelectElement>();
// State // State
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false);
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false);
const [element, setElement] = useState(-1) const [element, setElement] = useState(-1);
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1) const [primaryAxModifier, setPrimaryAxModifier] = useState(-1);
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1) const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1);
const [primaryAxValue, setPrimaryAxValue] = useState(0.0) const [primaryAxValue, setPrimaryAxValue] = useState(0.0);
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0) const [secondaryAxValue, setSecondaryAxValue] = useState(0.0);
function receiveAxValues( function receiveAxValues(
primaryAxModifier: number, primaryAxModifier: number,
@ -71,64 +73,64 @@ const WeaponModal = (props: Props) => {
secondaryAxModifier: number, secondaryAxModifier: number,
secondaryAxValue: number secondaryAxValue: number
) { ) {
setPrimaryAxModifier(primaryAxModifier) setPrimaryAxModifier(primaryAxModifier);
setSecondaryAxModifier(secondaryAxModifier) setSecondaryAxModifier(secondaryAxModifier);
setPrimaryAxValue(primaryAxValue) setPrimaryAxValue(primaryAxValue);
setSecondaryAxValue(secondaryAxValue) setSecondaryAxValue(secondaryAxValue);
} }
function receiveAxValidity(isValid: boolean) { function receiveAxValidity(isValid: boolean) {
setFormValid(isValid) setFormValid(isValid);
} }
function receiveElementValue(element: string) { function receiveElementValue(element: string) {
setElement(parseInt(element)) setElement(parseInt(element));
} }
function prepareObject() { 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)) 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)) 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) 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) { if (props.gridWeapon.object.ax > 0) {
object.weapon.ax_modifier1 = primaryAxModifier object.weapon.ax_modifier1 = primaryAxModifier;
object.weapon.ax_modifier2 = secondaryAxModifier object.weapon.ax_modifier2 = secondaryAxModifier;
object.weapon.ax_strength1 = primaryAxValue object.weapon.ax_strength1 = primaryAxValue;
object.weapon.ax_strength2 = secondaryAxValue object.weapon.ax_strength2 = secondaryAxValue;
} }
return object return object;
} }
async function updateWeapon() { async function updateWeapon() {
const updateObject = prepareObject() const updateObject = prepareObject();
return await api.endpoints.grid_weapons return await api.endpoints.grid_weapons
.update(props.gridWeapon.id, updateObject, headers) .update(props.gridWeapon.id, updateObject, headers)
.then((response) => processResult(response)) .then((response) => processResult(response))
.catch((error) => processError(error)) .catch((error) => processError(error));
} }
function processResult(response: AxiosResponse) { 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 if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon;
else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon;
setOpen(false) setOpen(false);
} }
function processError(error: any) { function processError(error: any) {
console.error(error) console.error(error);
} }
const elementSelect = () => { const elementSelect = () => {
@ -140,8 +142,8 @@ const WeaponModal = (props: Props) => {
sendValue={receiveElementValue} sendValue={receiveElementValue}
/> />
</section> </section>
) );
} };
const keySelect = () => { const keySelect = () => {
return ( return (
@ -192,8 +194,8 @@ const WeaponModal = (props: Props) => {
"" ""
)} )}
</section> </section>
) );
} };
const axSelect = () => { const axSelect = () => {
return ( return (
@ -206,12 +208,12 @@ const WeaponModal = (props: Props) => {
sendValues={receiveAxValues} sendValues={receiveAxValues}
/> />
</section> </section>
) );
} };
function openChange(open: boolean) { function openChange(open: boolean) {
setFormValid(false) setFormValid(false);
setOpen(open) setOpen(open);
} }
return ( return (
@ -255,7 +257,7 @@ const WeaponModal = (props: Props) => {
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
) );
} };
export default WeaponModal export default WeaponModal;

View file

@ -37,11 +37,11 @@
.stars { .stars {
display: inline-block; display: inline-block;
color: #FFA15E; color: #ffa15e;
font-size: $font-xlarge; font-size: $font-xlarge;
& > span { & > span {
color: #65DAFF; color: #65daff;
} }
} }

View file

@ -1,28 +1,72 @@
import React from 'react' import React from "react";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from "~components/WeaponLabelIcon";
import './index.scss' import "./index.scss";
interface Props { interface Props {
data: Weapon data: Weapon;
onClick: () => void 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'] const Proficiency = [
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'] "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",
];
const WeaponResult = (props: Props) => { const WeaponResult = (props: Props) => {
const router = useRouter() const router = useRouter();
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
const weapon = props.data router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const weapon = props.data;
return ( return (
<li className="WeaponResult" onClick={props.onClick}> <li className="WeaponResult" onClick={props.onClick}>
<img alt={weapon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} /> <img
alt={weapon.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`}
/>
<div className="Info"> <div className="Info">
<h5>{weapon.name[locale]}</h5> <h5>{weapon.name[locale]}</h5>
<UncapIndicator <UncapIndicator
@ -37,7 +81,7 @@ const WeaponResult = (props: Props) => {
</div> </div>
</div> </div>
</li> </li>
) );
} };
export default WeaponResult export default WeaponResult;

View file

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

View file

@ -1,37 +1,39 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import classnames from "classnames" import classnames from "classnames";
import SearchModal from "~components/SearchModal" import SearchModal from "~components/SearchModal";
import WeaponModal from "~components/WeaponModal" import WeaponModal from "~components/WeaponModal";
import WeaponHovercard from "~components/WeaponHovercard" import WeaponHovercard from "~components/WeaponHovercard";
import UncapIndicator from "~components/UncapIndicator" import UncapIndicator from "~components/UncapIndicator";
import Button from "~components/Button" import Button from "~components/Button";
import { ButtonType } from "~utils/enums" import { ButtonType } from "~utils/enums";
import type { SearchableObject } from "~types" import type { SearchableObject } from "~types";
import PlusIcon from "~public/icons/Add.svg" import PlusIcon from "~public/icons/Add.svg";
import "./index.scss" import "./index.scss";
interface Props { interface Props {
gridWeapon: GridWeapon | undefined gridWeapon: GridWeapon | undefined;
unitType: 0 | 1 unitType: 0 | 1;
position: number position: number;
editable: boolean editable: boolean;
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void;
} }
const WeaponUnit = (props: Props) => { 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 = const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en" router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const classes = classnames({ const classes = classnames({
WeaponUnit: true, WeaponUnit: true,
@ -39,48 +41,48 @@ const WeaponUnit = (props: Props) => {
grid: props.unitType == 1, grid: props.unitType == 1,
editable: props.editable, editable: props.editable,
filled: props.gridWeapon !== undefined, filled: props.gridWeapon !== undefined,
}) });
const gridWeapon = props.gridWeapon const gridWeapon = props.gridWeapon;
const weapon = gridWeapon?.object const weapon = gridWeapon?.object;
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl();
}) });
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = "";
if (props.gridWeapon) { if (props.gridWeapon) {
const weapon = props.gridWeapon.object! const weapon = props.gridWeapon.object!;
if (props.unitType == 0) { if (props.unitType == 0) {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element) 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 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 { } else {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element) 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 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) { function passUncapData(uncap: number) {
if (props.gridWeapon) if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, props.position, uncap) props.updateUncap(props.gridWeapon.id, props.position, uncap);
} }
function canBeModified(gridWeapon: GridWeapon) { function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object const weapon = gridWeapon.object;
return ( return (
weapon.ax > 0 || weapon.ax > 0 ||
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series)) (weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
) );
} }
const image = ( const image = (
@ -94,7 +96,7 @@ const WeaponUnit = (props: Props) => {
"" ""
)} )}
</div> </div>
) );
const editableImage = ( const editableImage = (
<SearchModal <SearchModal
@ -105,7 +107,7 @@ const WeaponUnit = (props: Props) => {
> >
{image} {image}
</SearchModal> </SearchModal>
) );
const unitContent = ( const unitContent = (
<div className={classes}> <div className={classes}>
@ -133,13 +135,13 @@ const WeaponUnit = (props: Props) => {
)} )}
<h3 className="WeaponName">{weapon?.name[locale]}</h3> <h3 className="WeaponName">{weapon?.name[locale]}</h3>
</div> </div>
) );
const withHovercard = ( const withHovercard = (
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard> <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,61 +1,61 @@
import React, { useCallback, useEffect, useState } from "react" import React, { useCallback, useEffect, useState } from "react";
import Head from "next/head" import Head from "next/head";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import { queryTypes, useQueryState } from "next-usequerystate" import { queryTypes, useQueryState } from "next-usequerystate";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { useTranslation } from "next-i18next" import { useTranslation } from "next-i18next";
import InfiniteScroll from "react-infinite-scroll-component" 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 api from "~utils/api";
import useDidMountEffect from "~utils/useDidMountEffect" import useDidMountEffect from "~utils/useDidMountEffect";
import { elements, allElement } from "~utils/Element" import { elements, allElement } from "~utils/Element";
import GridRep from "~components/GridRep" import GridRep from "~components/GridRep";
import GridRepCollection from "~components/GridRepCollection" import GridRepCollection from "~components/GridRepCollection";
import FilterBar from "~components/FilterBar" import FilterBar from "~components/FilterBar";
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next";
interface Props { interface Props {
user?: User user?: User;
teams?: { count: number; total_pages: number; results: Party[] } teams?: { count: number; total_pages: number; results: Party[] };
raids: Raid[] raids: Raid[];
sortedRaids: Raid[][] sortedRaids: Raid[][];
} }
const ProfileRoute: React.FC<Props> = (props: Props) => { const ProfileRoute: React.FC<Props> = (props: Props) => {
// Set up cookies // Set up cookies
const cookie = getCookie("account") const cookie = getCookie("account");
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null;
const headers = accountData const headers = accountData
? { Authorization: `Bearer ${accountData.token}` } ? { Authorization: `Bearer ${accountData.token}` }
: {} : {};
// Set up router // Set up router
const router = useRouter() const router = useRouter();
const { username } = router.query const { username } = router.query;
// Import translations // Import translations
const { t } = useTranslation("common") const { t } = useTranslation("common");
// Set up app-specific states // Set up app-specific states
const [raidsLoading, setRaidsLoading] = useState(true) const [raidsLoading, setRaidsLoading] = useState(true);
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false);
// Set up page-specific states // Set up page-specific states
const [parties, setParties] = useState<Party[]>([]) const [parties, setParties] = useState<Party[]>([]);
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>();
const [raid, setRaid] = useState<Raid>() const [raid, setRaid] = useState<Raid>();
// Set up infinite scrolling-related states // Set up infinite scrolling-related states
const [recordCount, setRecordCount] = useState(0) const [recordCount, setRecordCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1) const [totalPages, setTotalPages] = useState(1);
// Set up filter-specific query states // Set up filter-specific query states
// Recency is in seconds // Recency is in seconds
@ -63,57 +63,59 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
defaultValue: -1, defaultValue: -1,
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value), serialize: (value) => serializeElement(value),
}) });
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" }) const [raidSlug, setRaidSlug] = useQueryState("raid", {
defaultValue: "all",
});
const [recency, setRecency] = useQueryState( const [recency, setRecency] = useQueryState(
"recency", "recency",
queryTypes.integer.withDefault(-1) queryTypes.integer.withDefault(-1)
) );
// Define transformers for element // Define transformers for element
function parseElement(query: string) { function parseElement(query: string) {
let element: TeamElement | undefined = let element: TeamElement | undefined =
query === "all" query === "all"
? allElement ? allElement
: elements.find((element) => element.name.en.toLowerCase() === query) : elements.find((element) => element.name.en.toLowerCase() === query);
return element ? element.id : -1 return element ? element.id : -1;
} }
function serializeElement(value: number | undefined) { function serializeElement(value: number | undefined) {
let name = "" let name = "";
if (value != undefined) { if (value != undefined) {
if (value == -1) name = allElement.name.en.toLowerCase() if (value == -1) name = allElement.name.en.toLowerCase();
else name = elements[value].name.en.toLowerCase() else name = elements[value].name.en.toLowerCase();
} }
return name return name;
} }
// Set the initial parties from props // Set the initial parties from props
useEffect(() => { useEffect(() => {
if (props.teams) { if (props.teams) {
setTotalPages(props.teams.total_pages) setTotalPages(props.teams.total_pages);
setRecordCount(props.teams.count) setRecordCount(props.teams.count);
replaceResults(props.teams.count, props.teams.results) replaceResults(props.teams.count, props.teams.results);
} }
setCurrentPage(1) setCurrentPage(1);
}, []) }, []);
// Add scroll event listener for shadow on FilterBar on mount // Add scroll event listener for shadow on FilterBar on mount
useEffect(() => { useEffect(() => {
window.addEventListener("scroll", handleScroll) window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll) return () => window.removeEventListener("scroll", handleScroll);
}, []) }, []);
// Handle errors // Handle errors
const handleError = useCallback((error: any) => { const handleError = useCallback((error: any) => {
if (error.response != null) { if (error.response != null) {
console.error(error) console.error(error);
} else { } else {
console.error("There was an error.") console.error("There was an error.");
} }
}, []) }, []);
const fetchProfile = useCallback( const fetchProfile = useCallback(
({ replace }: { replace: boolean }) => { ({ replace }: { replace: boolean }) => {
@ -124,7 +126,7 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
recency: recency != -1 ? recency : undefined, recency: recency != -1 ? recency : undefined,
page: currentPage, page: currentPage,
}, },
} };
if (username && !Array.isArray(username)) { if (username && !Array.isArray(username)) {
api.endpoints.users api.endpoints.users
@ -133,62 +135,62 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
params: { ...filters, ...{ headers: headers } }, params: { ...filters, ...{ headers: headers } },
}) })
.then((response) => { .then((response) => {
setTotalPages(response.data.parties.total_pages) setTotalPages(response.data.parties.total_pages);
setRecordCount(response.data.parties.count) setRecordCount(response.data.parties.count);
if (replace) if (replace)
replaceResults( replaceResults(
response.data.parties.count, response.data.parties.count,
response.data.parties.results 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] [currentPage, parties, element, raid, recency]
) );
function replaceResults(count: number, list: Party[]) { function replaceResults(count: number, list: Party[]) {
if (count > 0) { 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 { } else {
setParties([]) setParties([]);
} }
} }
function appendResults(list: Party[]) { function appendResults(list: Party[]) {
setParties([...parties, ...list]) setParties([...parties, ...list]);
} }
// Fetch all raids on mount, then find the raid in the URL if present // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
api.endpoints.raids.getAll().then((response) => { api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) const cleanRaids: Raid[] = response.data.map((r: any) => r.raid);
setRaids(cleanRaids) setRaids(cleanRaids);
setRaidsLoading(false) setRaidsLoading(false);
const raid = cleanRaids.find((r) => r.slug === raidSlug) const raid = cleanRaids.find((r) => r.slug === raidSlug);
setRaid(raid) setRaid(raid);
return raid return raid;
}) });
}, [setRaids]) }, [setRaids]);
// When the element, raid or recency filter changes, // When the element, raid or recency filter changes,
// fetch all teams again. // fetch all teams again.
useDidMountEffect(() => { useDidMountEffect(() => {
setCurrentPage(1) setCurrentPage(1);
fetchProfile({ replace: true }) fetchProfile({ replace: true });
}, [element, raid, recency]) }, [element, raid, recency]);
// When the page changes, fetch all teams again. // When the page changes, fetch all teams again.
useDidMountEffect(() => { useDidMountEffect(() => {
// Current page changed // Current page changed
if (currentPage > 1) fetchProfile({ replace: false }) if (currentPage > 1) fetchProfile({ replace: false });
else if (currentPage == 1) fetchProfile({ replace: true }) else if (currentPage == 1) fetchProfile({ replace: true });
}, [currentPage]) }, [currentPage]);
// Receive filters from the filter bar // Receive filters from the filter bar
function receiveFilters({ function receiveFilters({
@ -196,30 +198,30 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
raidSlug, raidSlug,
recency, recency,
}: { }: {
element?: number element?: number;
raidSlug?: string raidSlug?: string;
recency?: number recency?: number;
}) { }) {
if (element == 0) setElement(0) if (element == 0) setElement(0);
else if (element) setElement(element) else if (element) setElement(element);
if (raids && raidSlug) { if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug) const raid = raids.find((raid) => raid.slug === raidSlug);
setRaid(raid) setRaid(raid);
setRaidSlug(raidSlug) setRaidSlug(raidSlug);
} }
if (recency) setRecency(recency) if (recency) setRecency(recency);
} }
// Methods: Navigation // Methods: Navigation
function handleScroll() { function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true) if (window.pageYOffset > 90) setScrolled(true);
else setScrolled(false) else setScrolled(false);
} }
function goTo(shortcode: string) { function goTo(shortcode: string) {
router.push(`/p/${shortcode}`) router.push(`/p/${shortcode}`);
} }
// TODO: Add save functions // TODO: Add save functions
@ -238,8 +240,8 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
key={`party-${i}`} key={`party-${i}`}
onClick={goTo} onClick={goTo}
/> />
) );
}) });
} }
return ( return (
@ -314,8 +316,8 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
)} )}
</section> </section>
</div> </div>
) );
} };
export const getServerSidePaths = async () => { export const getServerSidePaths = async () => {
return { return {
@ -324,8 +326,8 @@ export const getServerSidePaths = async () => {
{ params: { party: "string" } }, { params: { party: "string" } },
], ],
fallback: true, fallback: true,
} };
} };
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -410,22 +412,22 @@ const organizeRaids = (raids: Raid[]) => {
level: 0, level: 0,
group: 0, group: 0,
element: 0, element: 0,
} };
const numGroups = Math.max.apply( const numGroups = Math.max.apply(
Math, Math,
raids.map((raid) => raid.group) raids.map((raid) => raid.group)
) );
let groupedRaids = [] let groupedRaids = [];
for (let i = 0; i <= numGroups; i++) { for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i) groupedRaids[i] = raids.filter((raid) => raid.group == i);
} }
return { return {
raids: raids, raids: raids,
sortedRaids: groupedRaids, sortedRaids: groupedRaids,
} };
} };
export default ProfileRoute export default ProfileRoute;

View file

@ -1,40 +1,42 @@
import { useEffect } from "react" import { useEffect } from "react";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import { appWithTranslation } from "next-i18next" import { appWithTranslation } from "next-i18next";
import type { AppProps } from "next/app" import type { AppProps } from "next/app";
import Layout from "~components/Layout" 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) { function MyApp({ Component, pageProps }: AppProps) {
const cookie = getCookie("account") const cookie = getCookie("account");
const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null const cookieData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null;
useEffect(() => { useEffect(() => {
if (cookie) { if (cookie) {
console.log(`Logged in as user "${cookieData.username}"`) console.log(`Logged in as user "${cookieData.username}"`);
accountState.account.authorized = true accountState.account.authorized = true;
accountState.account.user = { accountState.account.user = {
id: cookieData.userId, id: cookieData.userId,
username: cookieData.username, username: cookieData.username,
picture: "", picture: "",
element: "", element: "",
gender: 0, gender: 0,
} };
} else { } else {
console.log(`You are not currently logged in.`) console.log(`You are not currently logged in.`);
} }
}, [cookie, cookieData]) }, [cookie, cookieData]);
return ( return (
<Layout> <Layout>
<Component {...pageProps} /> <Component {...pageProps} />
</Layout> </Layout>
) );
} }
export default appWithTranslation(MyApp) export default appWithTranslation(MyApp);

View file

@ -1,43 +1,43 @@
import React, { useEffect } from "react" import React, { useEffect } from "react";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations" import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Party from "~components/Party" import Party from "~components/Party";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import api from "~utils/api" import api from "~utils/api";
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next";
interface Props { interface Props {
jobs: Job[] jobs: Job[];
jobSkills: JobSkill[] jobSkills: JobSkill[];
raids: Raid[] raids: Raid[];
sortedRaids: Raid[][] sortedRaids: Raid[][];
} }
const NewRoute: React.FC<Props> = (props: Props) => { const NewRoute: React.FC<Props> = (props: Props) => {
function callback(path: string) { function callback(path: string) {
// This is scuffed, how do we do this natively? // This is scuffed, how do we do this natively?
window.history.replaceState(null, `Grid Tool`, `${path}`) window.history.replaceState(null, `Grid Tool`, `${path}`);
} }
useEffect(() => { useEffect(() => {
persistStaticData() persistStaticData();
}, [persistStaticData]) }, [persistStaticData]);
function persistStaticData() { function persistStaticData() {
appState.raids = props.raids appState.raids = props.raids;
appState.jobs = props.jobs appState.jobs = props.jobs;
appState.jobSkills = props.jobSkills appState.jobSkills = props.jobSkills;
} }
return ( return (
<div id="Content"> <div id="Content">
<Party new={true} raids={props.sortedRaids} pushHistory={callback} /> <Party new={true} raids={props.sortedRaids} pushHistory={callback} />
</div> </div>
) );
} };
export const getServerSidePaths = async () => { export const getServerSidePaths = async () => {
return { return {
@ -46,8 +46,8 @@ export const getServerSidePaths = async () => {
{ params: { party: "string" } }, { params: { party: "string" } },
], ],
fallback: true, fallback: true,
} };
} };
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -96,22 +96,22 @@ const organizeRaids = (raids: Raid[]) => {
level: 0, level: 0,
group: 0, group: 0,
element: 0, element: 0,
} };
const numGroups = Math.max.apply( const numGroups = Math.max.apply(
Math, Math,
raids.map((raid) => raid.group) raids.map((raid) => raid.group)
) );
let groupedRaids = [] let groupedRaids = [];
for (let i = 0; i <= numGroups; i++) { for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i) groupedRaids[i] = raids.filter((raid) => raid.group == i);
} }
return { return {
raids: raids, raids: raids,
sortedRaids: groupedRaids, sortedRaids: groupedRaids,
} };
} };
export default NewRoute export default NewRoute;

View file

@ -1,39 +1,39 @@
import React, { useEffect } from "react" import React, { useEffect } from "react";
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations" import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Party from "~components/Party" import Party from "~components/Party";
import { appState } from "~utils/appState" import { appState } from "~utils/appState";
import api from "~utils/api" import api from "~utils/api";
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next";
interface Props { interface Props {
party: Party party: Party;
jobs: Job[] jobs: Job[];
jobSkills: JobSkill[] jobSkills: JobSkill[];
raids: Raid[] raids: Raid[];
sortedRaids: Raid[][] sortedRaids: Raid[][];
} }
const PartyRoute: React.FC<Props> = (props: Props) => { const PartyRoute: React.FC<Props> = (props: Props) => {
useEffect(() => { useEffect(() => {
persistStaticData() persistStaticData();
}, [persistStaticData]) }, [persistStaticData]);
function persistStaticData() { function persistStaticData() {
appState.raids = props.raids appState.raids = props.raids;
appState.jobs = props.jobs appState.jobs = props.jobs;
appState.jobSkills = props.jobSkills appState.jobSkills = props.jobSkills;
} }
return ( return (
<div id="Content"> <div id="Content">
<Party team={props.party} raids={props.sortedRaids} /> <Party team={props.party} raids={props.sortedRaids} />
</div> </div>
) );
} };
export const getServerSidePaths = async () => { export const getServerSidePaths = async () => {
return { return {
@ -42,8 +42,8 @@ export const getServerSidePaths = async () => {
{ params: { party: "string" } }, { params: { party: "string" } },
], ],
fallback: true, fallback: true,
} };
} };
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
@ -104,22 +104,22 @@ const organizeRaids = (raids: Raid[]) => {
level: 0, level: 0,
group: 0, group: 0,
element: 0, element: 0,
} };
const numGroups = Math.max.apply( const numGroups = Math.max.apply(
Math, Math,
raids.map((raid) => raid.group) raids.map((raid) => raid.group)
) );
let groupedRaids = [] let groupedRaids = [];
for (let i = 0; i <= numGroups; i++) { for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i) groupedRaids[i] = raids.filter((raid) => raid.group == i);
} }
return { return {
raids: raids, raids: raids,
sortedRaids: groupedRaids, sortedRaids: groupedRaids,
} };
} };
export default PartyRoute export default PartyRoute;

View file

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

View file

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

View file

@ -1,6 +1,10 @@
@import '~meyer-reset-scss'; @import "~meyer-reset-scss";
html { html {
@include themed() {
background-color: t($background);
}
background: $background-color; background: $background-color;
font-size: 62.5%; font-size: 62.5%;
height: 100%; height: 100%;
@ -9,7 +13,8 @@ html {
body { body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
box-sizing: border-box; box-sizing: border-box;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: system-ui, -apple-system, "Helvetica Neue", Helvetica, Arial,
sans-serif;
font-size: 1.4rem; font-size: 1.4rem;
height: 100%; height: 100%;
padding: $unit * 2 !important; padding: $unit * 2 !important;
@ -55,11 +60,16 @@ a {
} }
} }
button, input { button,
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif; input {
font-family: system-ui, -apple-system, "Helvetica Neue", Helvetica, Arial,
sans-serif;
} }
h1, h2, h3, p { h1,
h2,
h3,
p {
color: $grey-00; color: $grey-00;
} }
@ -71,7 +81,7 @@ h1 {
select { select {
appearance: none; appearance: none;
background-image: url('/icons/Arrow.svg'); background-image: url("/icons/Arrow.svg");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position-y: center; background-position-y: center;
background-position-x: 97%; background-position-x: 97%;
@ -101,7 +111,6 @@ select {
min-width: auto; min-width: auto;
width: 100%; width: 100%;
} }
} }
.Overlay { .Overlay {
@ -117,7 +126,8 @@ select {
.Dialog { .Dialog {
$multiplier: 4; $multiplier: 4;
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running openModal; animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running
openModal;
background: white; background: white;
border-radius: $unit; border-radius: $unit;
display: flex; display: flex;
@ -279,7 +289,6 @@ select {
color: $dark-bg-light; color: $dark-bg-light;
} }
&.light { &.light {
color: $light-bg-light; color: $light-bg-light;
} }
@ -293,7 +302,8 @@ select {
} }
} }
#Teams, #Profile { #Teams,
#Profile {
display: flex; display: flex;
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
@ -306,7 +316,7 @@ select {
margin: auto; margin: auto;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;; align-items: center;
h2 { h2 {
color: $grey-60; color: $grey-60;
@ -373,4 +383,3 @@ i.tag {
transform: translate(-50%, -50%) scale(1); transform: translate(-50%, -50%) scale(1);
} }
} }

View file

@ -1,60 +1,68 @@
// @import 'include-media/dist/_include-media'; // @import 'include-media/dist/_include-media';
// Breakpoints // Breakpoints
$breakpoints: (small: 320px, medium: 800px, large: 1024px); $breakpoints: (
small: 320px,
medium: 800px,
large: 1024px,
);
$medium-screen: 800px; $medium-screen: 800px;
// Sizing // Sizing
$unit: 8px; $unit: 8px;
// Colors // Colors
$grey-00: #444; $grey-00: #222;
$grey-10: #444;
$grey-20: #555; $grey-20: #555;
$grey-30: #666;
$grey-40: #777; $grey-40: #777;
$grey-50: #888; $grey-50: #888;
$grey-60: #A9A9A9; $grey-60: #a9a9a9;
$grey-70: #C6C6C6; $grey-70: #c6c6c6;
$grey-80: #E9E9E9; $grey-80: #e9e9e9;
$grey-90: #F5F5F5; $grey-90: #f5f5f5;
$grey-100:#FAFAFA; $grey-100: #fafafa;
$background-color: $grey-90; $background-color: $grey-90;
$blue: #275DC5; $page--bg--light: $grey-90;
$red: #FF6161; $page--bg--dark: $grey-00;
$error: #D13A3A;
$blue: #275dc5;
$red: #ff6161;
$error: #d13a3a;
// Colors: Elements // Colors: Elements
$wind-text-dark: #009961; $wind-text-dark: #009961;
$wind-text-light: #1DC688; $wind-text-light: #1dc688;
$wind-bg-dark: #4CFFBF; $wind-bg-dark: #4cffbf;
$wind-bg-light: #CDFFED; $wind-bg-light: #cdffed;
$fire-text-dark: #990000; $fire-text-dark: #990000;
$fire-text-light: #EC5C5C; $fire-text-light: #ec5c5c;
$fire-bg-dark: #FF4D4D; $fire-bg-dark: #ff4d4d;
$fire-bg-light: #FFCDCD; $fire-bg-light: #ffcdcd;
$water-text-dark: #006199; $water-text-dark: #006199;
$water-text-light: #5CB7EC; $water-text-light: #5cb7ec;
$water-bg-dark: #4DBFFF; $water-bg-dark: #4dbfff;
$water-bg-light: #CDEDFF; $water-bg-light: #cdedff;
$earth-text-dark: #994000; $earth-text-dark: #994000;
$earth-text-light: #EC985C; $earth-text-light: #ec985c;
$earth-bg-dark: #FF974C; $earth-bg-dark: #ff974c;
$earth-bg-light: #FFE2CD; $earth-bg-light: #ffe2cd;
$light-text-dark: #998A00; $light-text-dark: #998a00;
$light-text-light: #C5B20C; $light-text-light: #c5b20c;
$light-bg-dark: #FFED4C; $light-bg-dark: #ffed4c;
$light-bg-light: #FFFACD; $light-bg-light: #fffacd;
$dark-text-dark: #8806B7; $dark-text-dark: #8806b7;
$dark-text-light: #C65CEC; $dark-text-light: #c65cec;
$dark-bg-dark: #D14CFF; $dark-bg-dark: #d14cff;
$dark-bg-light: #F2CDFF; $dark-bg-light: #f2cdff;
// Font weight
$normal: 400; $normal: 400;
$medium: 500; $medium: 500;
$bold: 600; $bold: 600;

View file

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

18
types/AxSkill.d.ts vendored
View file

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

62
types/Character.d.ts vendored
View file

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

View file

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

View file

@ -1,10 +1,10 @@
interface ElementState { interface ElementState {
[key: string]: CheckedState [key: string]: CheckedState;
null: CheckedState null: CheckedState;
wind: CheckedState wind: CheckedState;
fire: CheckedState fire: CheckedState;
water: CheckedState water: CheckedState;
earth: CheckedState earth: CheckedState;
dark: CheckedState dark: CheckedState;
light: 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 { interface GridCharacter {
id: string id: string;
position: number position: number;
object: Character object: Character;
uncap_level: number uncap_level: number;
} }

12
types/GridSummon.d.ts vendored
View file

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

16
types/GridWeapon.d.ts vendored
View file

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

24
types/Job.d.ts vendored
View file

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

26
types/JobSkill.d.ts vendored
View file

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

View file

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

42
types/Party.d.ts vendored
View file

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

View file

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

18
types/Raid.d.ts vendored
View file

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

View file

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

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