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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,42 +1,42 @@
#MainGrid { #MainGrid {
display: flex; display: flex;
justify-content: center; justify-content: center;
.grid_weapons { .grid_weapons {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr;
margin: 0; margin: 0;
padding: 0; padding: 0;
max-width: 528px; max-width: 528px;
} }
} }
#MainGrid, #ExtraGrid { #MainGrid,
.grid_weapons > * { #ExtraGrid {
margin-bottom: $unit * 3; .grid_weapons > * {
margin-right: $unit * 3; margin-bottom: $unit * 3;
margin-right: $unit * 3;
@media (max-width: $medium-screen) {
margin-bottom: $unit * 2;
margin-right: $unit * 2;
}
&:nth-last-child(-n+3) { @media (max-width: $medium-screen) {
margin-bottom: 0; margin-bottom: $unit * 2;
} margin-right: $unit * 2;
} }
.grid_weapons > *:nth-child(3n+3) { &:nth-last-child(-n + 3) {
margin-right: 0; margin-bottom: 0;
}
.grid_weapons > li {
list-style: none;
} }
}
.grid_weapons > *:nth-child(3n + 3) {
margin-right: 0;
}
.grid_weapons > li {
list-style: none;
}
} }
#ExtraWeapons #grid_weapons > * { #ExtraWeapons #grid_weapons > * {
margin-bottom: 0; margin-bottom: 0;
} }

View file

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

View file

@ -1,41 +1,41 @@
.Weapon.Hovercard { .Weapon.Hovercard {
.skills { .skills {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding-right: $unit * 2; padding-right: $unit * 2;
.axSkill { .axSkill {
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
&.primary img { &.primary img {
height: 64px; height: 64px;
width: 64px; width: 64px;
} }
&.secondary { &.secondary {
gap: $unit * 1.5; gap: $unit * 1.5;
img { img {
height: 36px; height: 36px;
width: 36px; width: 36px;
}
}
span {
font-size: $font-small;
font-weight: $medium;
text-align: center;
}
} }
} }
.weaponKeys { span {
display: flex; font-size: $font-small;
flex-direction: column; font-weight: $medium;
font-size: $normal; text-align: center;
gap: calc($unit / 2); }
} }
} }
.weaponKeys {
display: flex;
flex-direction: column;
font-size: $normal;
gap: calc($unit / 2);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +1,87 @@
import React from 'react' import React from "react";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from '~components/WeaponLabelIcon' import WeaponLabelIcon from "~components/WeaponLabelIcon";
import './index.scss' import "./index.scss";
interface Props { interface Props {
data: Weapon data: Weapon;
onClick: () => void onClick: () => void;
} }
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana'] const Proficiency = [
const Series = ['seraphic', 'grand', 'opus', 'draconic', 'revenant', 'primal', 'beast','regalia', 'omega', 'olden_primal', 'hollowsky', 'xeno', 'astral', 'rose', 'ultima', 'bahamut', 'epic', 'ennead', 'cosmos', 'ancestral', 'superlative', 'vintage', 'class_champion', 'sephira', 'new_world_foundation'] "none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
const Series = [
"seraphic",
"grand",
"opus",
"draconic",
"revenant",
"primal",
"beast",
"regalia",
"omega",
"olden_primal",
"hollowsky",
"xeno",
"astral",
"rose",
"ultima",
"bahamut",
"epic",
"ennead",
"cosmos",
"ancestral",
"superlative",
"vintage",
"class_champion",
"sephira",
"new_world_foundation",
];
const WeaponResult = (props: Props) => { const WeaponResult = (props: Props) => {
const router = useRouter() const router = useRouter();
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
const weapon = props.data router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const weapon = props.data;
return ( return (
<li className="WeaponResult" onClick={props.onClick}> <li className="WeaponResult" onClick={props.onClick}>
<img alt={weapon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} /> <img
<div className="Info"> alt={weapon.name[locale]}
<h5>{weapon.name[locale]}</h5> src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`}
<UncapIndicator />
type="weapon" <div className="Info">
flb={weapon.uncap.flb} <h5>{weapon.name[locale]}</h5>
ulb={weapon.uncap.ulb} <UncapIndicator
special={false} type="weapon"
/> flb={weapon.uncap.flb}
<div className="tags"> ulb={weapon.uncap.ulb}
<WeaponLabelIcon labelType={Element[weapon.element]} /> special={false}
<WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} /> />
</div> <div className="tags">
</div> <WeaponLabelIcon labelType={Element[weapon.element]} />
</li> <WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} />
) </div>
} </div>
</li>
);
};
export default WeaponResult export default WeaponResult;

View file

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

View file

@ -1,125 +1,125 @@
.WeaponUnit { .WeaponUnit {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
min-height: 139px; min-height: 139px;
position: relative; position: relative;
@media (max-width: $medium-screen) {
min-height: auto;
}
&:hover .Button {
display: block;
}
&.editable .WeaponImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
}
&.mainhand {
margin-right: $unit * 3;
max-width: 200px;
@media (max-width: $medium-screen) { @media (max-width: $medium-screen) {
min-height: auto; margin-right: $unit * 2;
}
&:hover .Button {
display: block;
} }
&.editable .WeaponImage:hover { &.editable .WeaponImage:hover {
border: $hover-stroke; transform: $scale-tall;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
}
&.mainhand {
margin-right: $unit * 3;
max-width: 200px;
@media (max-width: $medium-screen) {
margin-right: $unit * 2;
}
&.editable .WeaponImage:hover {
transform: $scale-tall;
}
.WeaponImage {
aspect-ratio: 200 / 418;
width: 200px;
height: auto;
@media (max-width: $medium-screen) {
width: 25vw;
}
}
}
&.grid {
max-width: 160px;
.WeaponImage {
aspect-ratio: 160 / 92;
list-style-type: none;
width: 160px;
height: auto;
@media (max-width: $medium-screen) {
width: 20vw;
}
}
}
&.filled h3 {
display: block;
}
&.filled ul {
display: flex;
}
& h3,
& ul {
display: none;
}
.Button {
background: white;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.14);
display: none;
position: absolute;
left: $unit;
top: $unit;
z-index: 3;
}
h3 {
color: $grey-00;
font-size: $font-button;
font-weight: $normal;
line-height: 1.1;
margin: 0;
text-align: center;
} }
.WeaponImage { .WeaponImage {
background: white; aspect-ratio: 200 / 418;
border: 1px solid rgba(0, 0, 0, 0); width: 200px;
border-radius: $unit; height: auto;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: calc($unit / 4);
overflow: hidden;
transition: all 0.18s ease-in-out;
&:hover .icon svg { @media (max-width: $medium-screen) {
fill: $grey-40; width: 25vw;
} }
img {
position: relative;
width: 100%;
z-index: 2;
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
svg {
fill: $grey-70;
}
}
} }
}
&.grid {
max-width: 160px;
.WeaponImage {
aspect-ratio: 160 / 92;
list-style-type: none;
width: 160px;
height: auto;
@media (max-width: $medium-screen) {
width: 20vw;
}
}
}
&.filled h3 {
display: block;
}
&.filled ul {
display: flex;
}
& h3,
& ul {
display: none;
}
.Button {
background: white;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.14);
display: none;
position: absolute;
left: $unit;
top: $unit;
z-index: 3;
}
h3 {
color: $grey-00;
font-size: $font-button;
font-weight: $normal;
line-height: 1.1;
margin: 0;
text-align: center;
}
.WeaponImage {
background: white;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: calc($unit / 4);
overflow: hidden;
transition: all 0.18s ease-in-out;
&:hover .icon svg {
fill: $grey-40;
}
img {
position: relative;
width: 100%;
z-index: 2;
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
svg {
fill: $grey-70;
}
}
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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