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

View file

@ -75,7 +75,7 @@
gap: $unit * 2;
select {
background: no-repeat url('/icons/ArrowDark.svg'), $grey-90;
background: no-repeat url("/icons/ArrowDark.svg"), $grey-90;
background-position-y: center;
background-position-x: 95%;
margin: 0;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,18 @@
import classNames from "classnames"
import React from "react"
import classNames from "classnames";
import React from "react";
import "./index.scss"
import "./index.scss";
interface Props {
children: React.ReactNode
children: React.ReactNode;
}
const GridRepCollection = (props: Props) => {
const classes = classNames({
GridRepCollection: true,
})
});
return <div className={classes}>{props.children}</div>
}
return <div className={classes}>{props.children}</div>;
};
export default GridRepCollection
export default GridRepCollection;

View file

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

View file

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

View file

@ -1,51 +1,51 @@
import React, { useEffect, useState } from "react"
import { getCookie, setCookie } from "cookies-next"
import { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
import React, { useEffect, useState } from "react";
import { getCookie, setCookie } from "cookies-next";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import Link from "next/link"
import * as Switch from "@radix-ui/react-switch"
import Link from "next/link";
import * as Switch from "@radix-ui/react-switch";
import AboutModal from "~components/AboutModal"
import AccountModal from "~components/AccountModal"
import LoginModal from "~components/LoginModal"
import SignupModal from "~components/SignupModal"
import AboutModal from "~components/AboutModal";
import AccountModal from "~components/AccountModal";
import LoginModal from "~components/LoginModal";
import SignupModal from "~components/SignupModal";
import "./index.scss"
import "./index.scss";
interface Props {
authenticated: boolean
username?: string
logout?: () => void
authenticated: boolean;
username?: string;
logout?: () => void;
}
const HeaderMenu = (props: Props) => {
const router = useRouter()
const { t } = useTranslation("common")
const router = useRouter();
const { t } = useTranslation("common");
const accountCookie = getCookie("account")
const accountCookie = getCookie("account");
const accountData: AccountCookie = accountCookie
? JSON.parse(accountCookie as string)
: null
: null;
const userCookie = getCookie("user")
const userCookie = getCookie("user");
const userData: UserCookie = userCookie
? JSON.parse(userCookie as string)
: null
: null;
const localeCookie = getCookie("NEXT_LOCALE")
const localeCookie = getCookie("NEXT_LOCALE");
const [checked, setChecked] = useState(false)
const [checked, setChecked] = useState(false);
useEffect(() => {
const locale = localeCookie
setChecked(locale === "ja" ? true : false)
}, [localeCookie])
const locale = localeCookie;
setChecked(locale === "ja" ? true : false);
}, [localeCookie]);
function handleCheckedChange(value: boolean) {
const language = value ? "ja" : "en"
setCookie("NEXT_LOCALE", language, { path: "/" })
router.push(router.asPath, undefined, { locale: language })
const language = value ? "ja" : "en";
setCookie("NEXT_LOCALE", language, { path: "/" });
router.push(router.asPath, undefined, { locale: language });
}
function authItems() {
@ -92,7 +92,7 @@ const HeaderMenu = (props: Props) => {
</div>
</ul>
</nav>
)
);
}
function unauthItems() {
@ -132,10 +132,10 @@ const HeaderMenu = (props: Props) => {
<SignupModal />
</div>
</ul>
)
);
}
return props.authenticated ? authItems() : unauthItems()
}
return props.authenticated ? authItems() : unauthItems();
};
export default HeaderMenu
export default HeaderMenu;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,36 @@
import React, { useEffect, useState } from "react"
import { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
import classnames from "classnames"
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import classnames from "classnames";
import SearchModal from "~components/SearchModal"
import SummonHovercard from "~components/SummonHovercard"
import UncapIndicator from "~components/UncapIndicator"
import PlusIcon from "~public/icons/Add.svg"
import SearchModal from "~components/SearchModal";
import SummonHovercard from "~components/SummonHovercard";
import UncapIndicator from "~components/UncapIndicator";
import PlusIcon from "~public/icons/Add.svg";
import type { SearchableObject } from "~types"
import type { SearchableObject } from "~types";
import "./index.scss"
import "./index.scss";
interface Props {
gridSummon: GridSummon | undefined
unitType: 0 | 1 | 2
position: number
editable: boolean
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
gridSummon: GridSummon | undefined;
unitType: 0 | 1 | 2;
position: number;
editable: boolean;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
}
const SummonUnit = (props: Props) => {
const { t } = useTranslation("common")
const { t } = useTranslation("common");
const [imageUrl, setImageUrl] = useState("")
const [imageUrl, setImageUrl] = useState("");
const router = useRouter()
const router = useRouter();
const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const classes = classnames({
SummonUnit: true,
@ -37,19 +39,19 @@ const SummonUnit = (props: Props) => {
friend: props.unitType == 2,
editable: props.editable,
filled: props.gridSummon !== undefined,
})
});
const gridSummon = props.gridSummon
const summon = gridSummon?.object
const gridSummon = props.gridSummon;
const summon = gridSummon?.object;
useEffect(() => {
generateImageUrl()
})
generateImageUrl();
});
function generateImageUrl() {
let imgSrc = ""
let imgSrc = "";
if (props.gridSummon) {
const summon = props.gridSummon.object!
const summon = props.gridSummon.object!;
const upgradedSummons = [
"2040094000",
@ -66,28 +68,28 @@ const SummonUnit = (props: Props) => {
"2040027000",
"2040046000",
"2040047000",
]
];
let suffix = ""
let suffix = "";
if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5
)
suffix = "_02"
suffix = "_02";
// Generate the correct source for the summon
if (props.unitType == 0 || props.unitType == 2)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`;
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`;
}
setImageUrl(imgSrc)
setImageUrl(imgSrc);
}
function passUncapData(uncap: number) {
if (props.gridSummon)
props.updateUncap(props.gridSummon.id, props.position, uncap)
props.updateUncap(props.gridSummon.id, props.position, uncap);
}
const image = (
@ -101,7 +103,7 @@ const SummonUnit = (props: Props) => {
""
)}
</div>
)
);
const editableImage = (
<SearchModal
@ -112,7 +114,7 @@ const SummonUnit = (props: Props) => {
>
{image}
</SearchModal>
)
);
const unitContent = (
<div className={classes}>
@ -131,13 +133,13 @@ const SummonUnit = (props: Props) => {
)}
<h3 className="SummonName">{summon?.name[locale]}</h3>
</div>
)
);
const withHovercard = (
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
)
);
return gridSummon && !props.editable ? withHovercard : unitContent
}
return gridSummon && !props.editable ? withHovercard : unitContent;
};
export default SummonUnit
export default SummonUnit;

View file

@ -1,5 +1,6 @@
.Fieldset textarea {
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;
}

View file

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

View file

@ -43,11 +43,11 @@
}
&-checkbox:checked + &-label {
background: #ECEBFF;
background: #ecebff;
}
&-checkbox:checked + &-label &-switch {
background: #8C86FF;
background: #8c86ff;
}
&-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 {
name: string
checked: boolean
editable: boolean
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
name: string;
checked: boolean;
editable: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const ToggleSwitch: React.FC<Props> = (props: Props) => {
@ -25,7 +25,7 @@ const ToggleSwitch: React.FC<Props> = (props: Props) => {
<span className="toggle-switch-switch" />
</label>
</div>
)
}
);
};
export default ToggleSwitch
export default ToggleSwitch;

View file

@ -1,97 +1,97 @@
import React from "react"
import { useSnapshot } from "valtio"
import { getCookie, deleteCookie } from "cookies-next"
import { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
import React from "react";
import { useSnapshot } from "valtio";
import { getCookie, deleteCookie } from "cookies-next";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import clonedeep from "lodash.clonedeep"
import clonedeep from "lodash.clonedeep";
import api from "~utils/api"
import { accountState, initialAccountState } from "~utils/accountState"
import { appState, initialAppState } from "~utils/appState"
import api from "~utils/api";
import { accountState, initialAccountState } from "~utils/accountState";
import { appState, initialAppState } from "~utils/appState";
import Header from "~components/Header"
import Button from "~components/Button"
import HeaderMenu from "~components/HeaderMenu"
import Header from "~components/Header";
import Button from "~components/Button";
import HeaderMenu from "~components/HeaderMenu";
const TopHeader = () => {
const { t } = useTranslation("common")
const { t } = useTranslation("common");
// Cookies
const accountCookie = getCookie("account")
const userCookie = getCookie("user")
const accountCookie = getCookie("account");
const userCookie = getCookie("user");
const headers = {}
const headers = {};
// accountCookies.account != null
// ? {
// Authorization: `Bearer ${accountCookies.account.access_token}`,
// }
// : {}
const { account } = useSnapshot(accountState)
const { party } = useSnapshot(appState)
const router = useRouter()
const { account } = useSnapshot(accountState);
const { party } = useSnapshot(appState);
const router = useRouter();
function copyToClipboard() {
const el = document.createElement("input")
el.value = window.location.href
el.id = "url-input"
document.body.appendChild(el)
const el = document.createElement("input");
el.value = window.location.href;
el.id = "url-input";
document.body.appendChild(el);
el.select()
document.execCommand("copy")
el.remove()
el.select();
document.execCommand("copy");
el.remove();
}
function newParty() {
// Push the root URL
router.push("/")
router.push("/");
// Clean state
const resetState = clonedeep(initialAppState)
const resetState = clonedeep(initialAppState);
Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key]
})
appState[key] = resetState[key];
});
// Set party to be editable
appState.party.editable = true
appState.party.editable = true;
}
function logout() {
deleteCookie("account")
deleteCookie("user")
deleteCookie("account");
deleteCookie("user");
// Clean state
const resetState = clonedeep(initialAccountState)
const resetState = clonedeep(initialAccountState);
Object.keys(resetState).forEach((key) => {
if (key !== "language") accountState[key] = resetState[key]
})
if (key !== "language") accountState[key] = resetState[key];
});
if (router.route != "/new") appState.party.editable = false
if (router.route != "/new") appState.party.editable = false;
router.push("/")
return false
router.push("/");
return false;
}
function toggleFavorite() {
if (party.favorited) unsaveFavorite()
else saveFavorite()
if (party.favorited) unsaveFavorite();
else saveFavorite();
}
function saveFavorite() {
if (party.id)
api.saveTeam({ id: party.id, params: headers }).then((response) => {
if (response.status == 201) appState.party.favorited = true
})
else console.error("Failed to save team: No party ID")
if (response.status == 201) appState.party.favorited = true;
});
else console.error("Failed to save team: No party ID");
}
function unsaveFavorite() {
if (party.id)
api.unsaveTeam({ id: party.id, params: headers }).then((response) => {
if (response.status == 200) appState.party.favorited = false
})
else console.error("Failed to unsave team: No party ID")
if (response.status == 200) appState.party.favorited = false;
});
else console.error("Failed to unsave team: No party ID");
}
const leftNav = () => {
@ -108,8 +108,8 @@ const TopHeader = () => {
<HeaderMenu authenticated={account.authorized} />
)}
</div>
)
}
);
};
const saveButton = () => {
if (party.favorited)
@ -117,14 +117,14 @@ const TopHeader = () => {
<Button icon="save" active={true} onClick={toggleFavorite}>
Saved
</Button>
)
);
else
return (
<Button icon="save" onClick={toggleFavorite}>
Save
</Button>
)
}
);
};
const rightNav = () => {
return (
@ -145,10 +145,10 @@ const TopHeader = () => {
{t("buttons.new")}
</Button>
</div>
)
}
);
};
return <Header position="top" left={leftNav()} right={rightNav()} />
}
return <Header position="top" left={leftNav()} right={rightNav()} />;
};
export default TopHeader
export default TopHeader;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,159 +1,226 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import React from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import * as HoverCard from '@radix-ui/react-hover-card'
import * as HoverCard from "@radix-ui/react-hover-card";
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from "~components/UncapIndicator";
import { axData } from '~utils/axData'
import { axData } from "~utils/axData";
import './index.scss'
import "./index.scss";
interface Props {
gridWeapon: GridWeapon
children: React.ReactNode
gridWeapon: GridWeapon;
children: React.ReactNode;
}
interface KeyNames {
[key: string]: {
[key: string]: string
en: string,
ja: string
}
[key: string]: string;
en: string;
ja: string;
};
}
const WeaponHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const router = useRouter();
const { t } = useTranslation("common");
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Proficiency = [
"none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
const WeaponKeyNames: KeyNames = {
'2': {
en: 'Pendulum',
ja: 'ペンデュラム'
"2": {
en: "Pendulum",
ja: "ペンデュラム",
},
'3': {
en: 'Teluma',
ja: 'テルマ'
"3": {
en: "Teluma",
ja: "テルマ",
},
'17': {
en: 'Gauph Key',
ja: 'ガフスキー'
"17": {
en: "Gauph Key",
ja: "ガフスキー",
},
'22': {
en: 'Emblem',
ja: 'エンブレム'
}
}
"22": {
en: "Emblem",
ja: "エンブレム",
},
};
const tintElement = (props.gridWeapon.object.element == 0 && props.gridWeapon.element) ? Element[props.gridWeapon.element] : Element[props.gridWeapon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(' ', '_')}`
const tintElement =
props.gridWeapon.object.element == 0 && props.gridWeapon.element
? Element[props.gridWeapon.element]
: Element[props.gridWeapon.object.element];
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(
" ",
"_"
)}`;
const hovercardSide = () => {
if (props.gridWeapon.position == -1)
return "right"
if (props.gridWeapon.position == -1) return "right";
else if ([6, 7, 8, 9, 10, 11].includes(props.gridWeapon.position))
return "top"
else
return "bottom"
}
return "top";
else return "bottom";
};
const createPrimaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1]
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1];
if (props.gridWeapon.ax) {
const simpleAxSkill = props.gridWeapon.ax[0]
const axSkill = primaryAxSkills.find(skill => skill.id == simpleAxSkill.modifier)
const simpleAxSkill = props.gridWeapon.ax[0];
const axSkill = primaryAxSkills.find(
(skill) => skill.id == simpleAxSkill.modifier
);
return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${ (axSkill?.suffix) ? axSkill.suffix : '' }`
return `${axSkill?.name[locale]} +${simpleAxSkill.strength}${
axSkill?.suffix ? axSkill.suffix : ""
}`;
}
return ''
}
return "";
};
const createSecondaryAxSkillString = () => {
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1]
const primaryAxSkills = axData[props.gridWeapon.object.ax - 1];
if (props.gridWeapon.ax) {
const primarySimpleAxSkill = props.gridWeapon.ax[0]
const secondarySimpleAxSkill = props.gridWeapon.ax[1]
const primarySimpleAxSkill = props.gridWeapon.ax[0];
const secondarySimpleAxSkill = props.gridWeapon.ax[1];
const primaryAxSkill = primaryAxSkills.find(skill => skill.id == primarySimpleAxSkill.modifier)
const primaryAxSkill = primaryAxSkills.find(
(skill) => skill.id == primarySimpleAxSkill.modifier
);
if (primaryAxSkill && primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondarySimpleAxSkill.modifier)
return `${secondaryAxSkill?.name[locale]} +${secondarySimpleAxSkill.strength}${ (secondaryAxSkill?.suffix) ? secondaryAxSkill.suffix : '' }`
const secondaryAxSkill = primaryAxSkill.secondary.find(
(skill) => skill.id == secondarySimpleAxSkill.modifier
);
return `${secondaryAxSkill?.name[locale]} +${
secondarySimpleAxSkill.strength
}${secondaryAxSkill?.suffix ? secondaryAxSkill.suffix : ""}`;
}
}
return ''
}
return "";
};
function weaponImage() {
const weapon = props.gridWeapon.object
const weapon = props.gridWeapon.object;
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`;
else
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`;
}
const keysSection = (
<section className="weaponKeys">
{ (WeaponKeyNames[props.gridWeapon.object.series]) ?
<h5 className={tintElement}>{ WeaponKeyNames[props.gridWeapon.object.series][locale] }{ (locale === 'en') ? 's' : '' }</h5> : ''
}
{WeaponKeyNames[props.gridWeapon.object.series] ? (
<h5 className={tintElement}>
{WeaponKeyNames[props.gridWeapon.object.series][locale]}
{locale === "en" ? "s" : ""}
</h5>
) : (
""
)}
{ (props.gridWeapon.weapon_keys) ?
Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
{props.gridWeapon.weapon_keys
? Array.from(Array(props.gridWeapon.weapon_keys.length)).map((x, i) => {
return (
<div className="weaponKey" key={props.gridWeapon.weapon_keys![i].id}>
<div
className="weaponKey"
key={props.gridWeapon.weapon_keys![i].id}
>
<span>{props.gridWeapon.weapon_keys![i].name[locale]}</span>
</div>
)
}) : '' }
);
})
: ""}
</section>
)
);
const axSection = (
<section className="axSkills">
<h5 className={tintElement}>{t('modals.weapon.subtitles.ax_skills')}</h5>
<h5 className={tintElement}>{t("modals.weapon.subtitles.ax_skills")}</h5>
<div className="skills">
<div className="primary axSkill">
<img alt="AX1" src={`/icons/ax/primary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[0].modifier : '' }.png`} />
<img
alt="AX1"
src={`/icons/ax/primary_${
props.gridWeapon.ax ? props.gridWeapon.ax[0].modifier : ""
}.png`}
/>
<span>{createPrimaryAxSkillString()}</span>
</div>
{ (props.gridWeapon.ax && props.gridWeapon.ax[1].modifier && props.gridWeapon.ax[1].strength) ?
{props.gridWeapon.ax &&
props.gridWeapon.ax[1].modifier &&
props.gridWeapon.ax[1].strength ? (
<div className="secondary axSkill">
<img alt="AX2" src={`/icons/ax/secondary_${ (props.gridWeapon.ax) ? props.gridWeapon.ax[1].modifier : '' }.png`} />
<img
alt="AX2"
src={`/icons/ax/secondary_${
props.gridWeapon.ax ? props.gridWeapon.ax[1].modifier : ""
}.png`}
/>
<span>{createSecondaryAxSkillString()}</span>
</div> : ''}
</div>
) : (
""
)}
</div>
</section>
)
);
return (
<HoverCard.Root>
<HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}>
<div className="top">
<div className="title">
<h4>{ props.gridWeapon.object.name[locale] }</h4>
<img alt={props.gridWeapon.object.name[locale]} src={weaponImage()} />
<h4>{props.gridWeapon.object.name[locale]}</h4>
<img
alt={props.gridWeapon.object.name[locale]}
src={weaponImage()}
/>
</div>
<div className="subInfo">
<div className="icons">
{ (props.gridWeapon.object.element !== 0 || (props.gridWeapon.object.element === 0 && props.gridWeapon.element != null)) ?
<WeaponLabelIcon labelType={ (props.gridWeapon.object.element === 0 && props.gridWeapon.element !== 0) ? Element[props.gridWeapon.element] : Element[props.gridWeapon.object.element] } />
: '' }
<WeaponLabelIcon labelType={ Proficiency[props.gridWeapon.object.proficiency] } />
{props.gridWeapon.object.element !== 0 ||
(props.gridWeapon.object.element === 0 &&
props.gridWeapon.element != null) ? (
<WeaponLabelIcon
labelType={
props.gridWeapon.object.element === 0 &&
props.gridWeapon.element !== 0
? Element[props.gridWeapon.element]
: Element[props.gridWeapon.object.element]
}
/>
) : (
""
)}
<WeaponLabelIcon
labelType={Proficiency[props.gridWeapon.object.proficiency]}
/>
</div>
<UncapIndicator
type="weapon"
@ -164,14 +231,22 @@ const WeaponHovercard = (props: Props) => {
</div>
</div>
{ (props.gridWeapon.object.ax > 0 && props.gridWeapon.ax && props.gridWeapon.ax[0].modifier && props.gridWeapon.ax[0].strength ) ? axSection : '' }
{ (props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0) ? keysSection : '' }
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
{props.gridWeapon.object.ax > 0 &&
props.gridWeapon.ax &&
props.gridWeapon.ax[0].modifier &&
props.gridWeapon.ax[0].strength
? axSection
: ""}
{props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0
? keysSection
: ""}
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t("buttons.wiki")}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
)
}
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
interface Props {
currentValue?: WeaponKey
series: number
slot: number
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
currentValue?: WeaponKey;
series: number;
slot: number;
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}
const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
const [keys, setKeys] = useState<WeaponKey[][]>([])
const [currentKey, setCurrentKey] = useState('')
const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) {
const [keys, setKeys] = useState<WeaponKey[][]>([]);
const [currentKey, setCurrentKey] = useState("");
const pendulumNames = [
{ en: 'Pendulum', jp: '' },
{ en: 'Chain', jp: '' }
]
{ en: "Pendulum", jp: "" },
{ en: "Chain", jp: "" },
];
const telumaNames = [ { en: 'Teluma', jp: '' } ]
const emblemNames = [ { en: 'Emblem', jp: '' } ]
const telumaNames = [{ en: "Teluma", jp: "" }];
const emblemNames = [{ en: "Emblem", jp: "" }];
const gauphNames = [
{ en: 'Gauph Key', jp: '' },
{ en: 'Ultima Key', jp: '' },
{ en: 'Gate of Omnipotence', jp: '' }
]
{ en: "Gauph Key", jp: "" },
{ en: "Ultima Key", jp: "" },
{ en: "Gate of Omnipotence", jp: "" },
];
useEffect(() => {
if (props.currentValue)
setCurrentKey(props.currentValue.id)
}, [props.currentValue])
if (props.currentValue) setCurrentKey(props.currentValue.id);
}, [props.currentValue]);
useEffect(() => {
const filterParams = {
params: {
series: props.series,
slot: props.slot
}
}
slot: props.slot,
},
};
function organizeWeaponKeys(weaponKeys: WeaponKey[]) {
const numGroups = Math.max.apply(Math, weaponKeys.map(key => key.group))
let groupedKeys = []
const numGroups = Math.max.apply(
Math,
weaponKeys.map((key) => key.group)
);
let groupedKeys = [];
for (let i = 0; i <= numGroups; i++) {
groupedKeys[i] = weaponKeys.filter(key => key.group == i)
groupedKeys[i] = weaponKeys.filter((key) => key.group == i);
}
setKeys(groupedKeys)
setKeys(groupedKeys);
}
function fetchWeaponKeys() {
api.endpoints.weapon_keys.getAll(filterParams)
.then((response) => {
const keys = response.data.map((k: any) => k.weapon_key)
organizeWeaponKeys(keys)
})
api.endpoints.weapon_keys.getAll(filterParams).then((response) => {
const keys = response.data.map((k: any) => k.weapon_key);
organizeWeaponKeys(keys);
});
}
fetchWeaponKeys()
}, [props.series, props.slot])
fetchWeaponKeys();
}, [props.series, props.slot]);
function weaponKeyGroup(index: number) {
['α','β','γ','Δ'].sort((a, b) => a.localeCompare(b, 'el'))
["α", "β", "γ", "Δ"].sort((a, b) => a.localeCompare(b, "el"));
const sortByOrder = (a: WeaponKey, b: WeaponKey) => a.order > b.order ? 1 : -1
const sortByOrder = (a: WeaponKey, b: WeaponKey) =>
a.order > b.order ? 1 : -1;
const options = keys && keys.length > 0 && keys[index].length > 0 &&
const options =
keys &&
keys.length > 0 &&
keys[index].length > 0 &&
keys[index].sort(sortByOrder).map((item, i) => {
return (
<option key={i} value={item.id}>{item.name.en}</option>
)
})
<option key={i} value={item.id}>
{item.name.en}
</option>
);
});
let name: { [key: string]: string } = {}
if (props.series == 2 && index == 0)
name = pendulumNames[0]
let name: { [key: string]: string } = {};
if (props.series == 2 && index == 0) name = pendulumNames[0];
else if (props.series == 2 && props.slot == 1 && index == 1)
name = pendulumNames[1]
else if (props.series == 3)
name = telumaNames[index]
else if (props.series == 17)
name = gauphNames[props.slot]
else if (props.series == 22)
name = emblemNames[index]
name = pendulumNames[1];
else if (props.series == 3) name = telumaNames[index];
else if (props.series == 17) name = gauphNames[props.slot];
else if (props.series == 22) name = emblemNames[index];
return (
<optgroup key={index} label={ (props.series == 17 && props.slot == 2) ? name.en : `${name.en}s`}>
<optgroup
key={index}
label={
props.series == 17 && props.slot == 2 ? name.en : `${name.en}s`
}
>
{options}
</optgroup>
)
);
}
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (props.onChange)
props.onChange(event)
if (props.onChange) props.onChange(event);
setCurrentKey(event.currentTarget.value)
setCurrentKey(event.currentTarget.value);
}
const emptyOption = () => {
let name = ''
if (props.series == 2)
name = pendulumNames[0].en
else if (props.series == 3)
name = telumaNames[0].en
else if (props.series == 17)
name = gauphNames[props.slot].en
else if (props.series == 22)
name = emblemNames[0].en
let name = "";
if (props.series == 2) name = pendulumNames[0].en;
else if (props.series == 3) name = telumaNames[0].en;
else if (props.series == 17) name = gauphNames[props.slot].en;
else if (props.series == 22) name = emblemNames[0].en;
return `No ${name}`
}
return `No ${name}`;
};
return (
<select
@ -122,13 +126,17 @@ const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function us
value={currentKey}
onBlur={props.onBlur}
onChange={handleChange}
ref={ref}>
<option key="-1" value="-1">{ emptyOption() }</option>
{ Array.from(Array(keys?.length)).map((x, i) => {
return weaponKeyGroup(i)
ref={ref}
>
<option key="-1" value="-1">
{emptyOption()}
</option>
{Array.from(Array(keys?.length)).map((x, i) => {
return weaponKeyGroup(i);
})}
</select>
)
})
);
}
);
export default WeaponKeyDropdown
export default WeaponKeyDropdown;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,28 +1,72 @@
import React from 'react'
import { useRouter } from 'next/router'
import React from "react";
import { useRouter } from "next/router";
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import './index.scss'
import "./index.scss";
interface Props {
data: Weapon
onClick: () => void
data: Weapon;
onClick: () => void;
}
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['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 Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Proficiency = [
"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 router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const weapon = props.data
const router = useRouter();
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const weapon = props.data;
return (
<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">
<h5>{weapon.name[locale]}</h5>
<UncapIndicator
@ -37,7 +81,7 @@ const WeaponResult = (props: Props) => {
</div>
</div>
</li>
)
}
);
};
export default WeaponResult
export default WeaponResult;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

18
types/AxSkill.d.ts vendored
View file

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

62
types/Character.d.ts vendored
View file

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

View file

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

View file

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

View file

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

View file

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

12
types/GridSummon.d.ts vendored
View file

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

16
types/GridWeapon.d.ts vendored
View file

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

24
types/Job.d.ts vendored
View file

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

26
types/JobSkill.d.ts vendored
View file

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

View file

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

View file

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

18
types/Raid.d.ts vendored
View file

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

View file

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

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