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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,48 +1,48 @@
.AXSelect {
display: flex;
flex-direction: column;
gap: $unit;
display: flex;
flex-direction: column;
gap: $unit;
.AXSet {
&.hidden {
display: none;
}
.errors {
color: $error;
display: none;
padding: $unit 0;
&.visible {
display: block;
}
}
.fields {
display: flex;
flex-direction: row;
gap: $unit;
select {
flex-grow: 1;
margin: 0;
}
.Input {
-webkit-font-smoothing: antialiased;
border: none;
background-color: $grey-90;
border-radius: 6px;
box-sizing: border-box;
color: $grey-00;
height: $unit * 6;
display: block;
font-size: $font-regular;
padding: $unit;
text-align: right;
min-width: 100px;
width: 100px;
}
}
.AXSet {
&.hidden {
display: none;
}
.errors {
color: $error;
display: none;
padding: $unit 0;
&.visible {
display: block;
}
}
.fields {
display: flex;
flex-direction: row;
gap: $unit;
select {
flex-grow: 1;
margin: 0;
}
.Input {
-webkit-font-smoothing: antialiased;
border: none;
background-color: $grey-90;
border-radius: 6px;
box-sizing: border-box;
color: $grey-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 { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import classNames from 'classnames'
import classNames from "classnames";
import { axData } from '~utils/axData'
import { axData } from "~utils/axData";
import './index.scss'
import "./index.scss";
interface ErrorMap {
[index: string]: string
axValue1: string
axValue2: string
[index: string]: string;
axValue1: string;
axValue2: string;
}
interface Props {
axType: number
currentSkills?: SimpleAxSkill[],
sendValidity: (isValid: boolean) => void
sendValues: (primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) => void
axType: number;
currentSkills?: SimpleAxSkill[];
sendValidity: (isValid: boolean) => void;
sendValues: (
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) => void;
}
const AXSelect = (props: Props) => {
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const { t } = useTranslation('common')
const router = useRouter();
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const { t } = useTranslation("common");
// Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({
axValue1: '',
axValue2: ''
})
// Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({
axValue1: "",
axValue2: "",
});
const primaryErrorClasses = classNames({
'errors': true,
'visible': errors.axValue1.length > 0
})
const primaryErrorClasses = classNames({
errors: true,
visible: errors.axValue1.length > 0,
});
const secondaryErrorClasses = classNames({
'errors': true,
'visible': errors.axValue2.length > 0
})
const secondaryErrorClasses = classNames({
errors: true,
visible: errors.axValue2.length > 0,
});
// Refs
const primaryAxModifierSelect = React.createRef<HTMLSelectElement>()
const primaryAxValueInput = React.createRef<HTMLInputElement>()
const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>()
const secondaryAxValueInput = React.createRef<HTMLInputElement>()
// Refs
const primaryAxModifierSelect = React.createRef<HTMLSelectElement>();
const primaryAxValueInput = React.createRef<HTMLInputElement>();
const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>();
const secondaryAxValueInput = React.createRef<HTMLInputElement>();
// States
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
// States
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1);
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1);
const [primaryAxValue, setPrimaryAxValue] = useState(0.0);
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0);
useEffect(() => {
if (props.currentSkills && props.currentSkills[0]) {
if (props.currentSkills[0].modifier != null)
setPrimaryAxModifier(props.currentSkills[0].modifier)
useEffect(() => {
if (props.currentSkills && props.currentSkills[0]) {
if (props.currentSkills[0].modifier != null)
setPrimaryAxModifier(props.currentSkills[0].modifier);
setPrimaryAxValue(props.currentSkills[0].strength)
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]) {
if (props.currentSkills[1].modifier != null)
setSecondaryAxModifier(props.currentSkills[1].modifier)
axOptionElements?.unshift(
<option key={-1} value={-1}>
{t("ax.no_skill")}
</option>
);
return axOptionElements;
}
setSecondaryAxValue(props.currentSkills[1].strength)
}
}, [props.currentSkills])
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const value = parseInt(event.target.value);
useEffect(() => {
props.sendValues(primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue)
}, [props, primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue])
if (primaryAxModifierSelect.current == event.target) {
setPrimaryAxModifier(value);
useEffect(() => {
props.sendValidity(primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === '')
}, [props, primaryAxValue, errors])
if (
primaryAxValueInput.current &&
secondaryAxModifierSelect.current &&
secondaryAxValueInput.current
) {
setupInput(
axData[props.axType - 1][value],
primaryAxValueInput.current
);
// Classes
const secondarySetClasses = classNames({
'AXSet': true,
'hidden': primaryAxModifier < 0
})
secondaryAxModifierSelect.current.value = "-1";
secondaryAxValueInput.current.value = "";
}
} else {
setSecondaryAxModifier(value);
function generateOptions(modifierSet: number) {
const axOptions = axData[props.axType - 1]
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier];
const currentAxSkill = primaryAxSkill.secondary
? primaryAxSkill.secondary.find((skill) => skill.id == value)
: undefined;
let axOptionElements: React.ReactNode[] = []
if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name[locale]}</option>
)
})
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 {
// 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
newErrors.axValue2 = "";
}
}
}
if (modifier >= 0 && axOptions[modifier]) {
const primarySkill = axOptions[modifier]
setErrors(newErrors);
if (primarySkill.secondary) {
const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name[locale]}</option>
)
})
}
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
}
}
axOptionElements?.unshift(<option key={-1} value={-1}>{t('ax.no_skill')}</option>)
return axOptionElements
}
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const value = parseInt(event.target.value)
if (primaryAxModifierSelect.current == event.target) {
setPrimaryAxModifier(value)
if (primaryAxValueInput.current && secondaryAxModifierSelect.current && secondaryAxValueInput.current) {
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
secondaryAxModifierSelect.current.value = "-1"
secondaryAxValueInput.current.value = ""
onChange={handleSelectChange}
ref={primaryAxModifierSelect}
>
{generateOptions(0)}
</select>
<input
defaultValue={
props.currentSkills && props.currentSkills[0]
? props.currentSkills[0].strength
: 0
}
} else {
setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
const currentAxSkill = (primaryAxSkill.secondary) ?
primaryAxSkill.secondary.find(skill => skill.id == value) : undefined
if (secondaryAxValueInput.current)
setupInput(currentAxSkill, secondaryAxValueInput.current)
}
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value)
let newErrors = {...errors}
if (primaryAxValueInput.current == event.target) {
if (handlePrimaryErrors(value))
setPrimaryAxValue(value)
} else {
if (handleSecondaryErrors(value))
setSecondaryAxValue(value)
}
}
function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors}
if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = t('ax.errors.value_too_low', {
name: primaryAxSkill.name[locale],
minValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
})
} else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = t('ax.errors.value_too_high', {
name: primaryAxSkill.name[locale],
maxValue: primaryAxSkill.minValue,
suffix: (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''
})
} else if (!value || value <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else {
newErrors.axValue1 = ''
}
setErrors(newErrors)
return newErrors.axValue1.length === 0
}
function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors}
if (primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondaryAxModifier)
if (secondaryAxSkill) {
if (value < secondaryAxSkill.minValue) {
newErrors.axValue2 = t('ax.errors.value_too_low', {
name: secondaryAxSkill.name[locale],
minValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
})
} else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = t('ax.errors.value_too_high', {
name: secondaryAxSkill.name[locale],
maxValue: secondaryAxSkill.minValue,
suffix: (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''
})
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = t('ax.errors.value_not_whole', { name: secondaryAxSkill.name[locale] })
} else if (primaryAxValue <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { name: primaryAxSkill.name[locale] })
} else {
newErrors.axValue2 = ''
}
}
}
setErrors(newErrors)
return newErrors.axValue2.length === 0
}
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
if (ax) {
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`
element.disabled = false
element.placeholder = rangeString
element.min = `${ax.minValue}`
element.max = `${ax.maxValue}`
element.step = (ax.suffix) ? "0.5" : "1"
} else {
if (primaryAxValueInput.current && secondaryAxValueInput.current) {
if (primaryAxValueInput.current == element) {
primaryAxValueInput.current.disabled = true
primaryAxValueInput.current.placeholder = ''
}
secondaryAxValueInput.current.disabled = true
secondaryAxValueInput.current.placeholder = ''
}
}
}
return (
<div className="AXSelect">
<div className="AXSet">
<div className="fields">
<select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled={primaryAxValue != 0} />
</div>
<p className={primaryErrorClasses}>{errors.axValue1}</p>
</div>
<div className={secondarySetClasses}>
<div className="fields">
<select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled={secondaryAxValue != 0} />
</div>
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div>
className="Input"
type="number"
onChange={handleInputChange}
ref={primaryAxValueInput}
disabled={primaryAxValue != 0}
/>
</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 {
align-items: center;
background: transparent;
border: none;
border-radius: 6px;
color: $grey-50;
display: inline-flex;
font-size: $font-button;
font-weight: $normal;
gap: 6px;
padding: 8px 12px;
align-items: center;
background: transparent;
border: none;
border-radius: 6px;
color: $grey-50;
display: inline-flex;
font-size: $font-button;
font-weight: $normal;
gap: 6px;
padding: 8px 12px;
&: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 {
background: white;
cursor: pointer;
color: $grey-00;
color: darken(#ff4d4d, 30);
.icon svg {
fill: $grey-00;
}
.icon svg {
fill: darken(#ff4d4d, 30);
stroke: darken(#ff4d4d, 30);
}
}
}
.icon.stroke svg {
fill: none;
stroke: $grey-00;
}
&.modal:hover {
background: $grey-90;
}
&.modal.destructive {
color: $error;
&:hover {
color: darken($error, 10);
}
}
.icon {
margin-top: 2px;
svg {
fill: $grey-50;
height: 12px;
width: 12px;
}
&.destructive:hover {
background: $error;
color: white;
.icon svg {
fill: white;
}
&.check svg {
margin-top: 1px;
height: 14px;
width: auto;
}
&.save:hover {
color: #FF4D4D;
.icon svg {
fill: #FF4D4D;
stroke: #FF4D4D;
}
&.stroke svg {
fill: none;
stroke: $grey-50;
}
&.save.Active {
color: #FF4D4D;
.icon svg {
fill: #FF4D4D;
stroke: #FF4D4D;
}
&:hover {
color: darken(#FF4D4D, 30);
.icon svg {
fill: darken(#FF4D4D, 30);
stroke: darken(#FF4D4D, 30);
}
}
&.settings svg {
height: 13px;
width: 13px;
}
}
&.modal:hover {
background: $grey-90;
&.Active {
background: white;
}
&.btn-blue {
background: $blue;
color: #8b8b8b;
&:hover {
background: #4b9be5;
color: #233e56;
}
}
&.modal.destructive {
color: $error;
&.btn-red {
background: #fa4242;
color: #860f0f;
&:hover {
color: darken($error, 10)
}
&:hover {
background: #e91a1a;
color: #4e1717;
.icon {
color: #4e1717;
}
}
.icon {
margin-top: 2px;
svg {
fill: $grey-50;
height: 12px;
width: 12px;
}
&.check svg {
margin-top: 1px;
height: 14px;
width: auto;
}
&.stroke svg {
fill: none;
stroke: $grey-50;
}
&.settings svg {
height: 13px;
width: 13px;
}
color: #860f0f;
}
}
&.Active {
background: white;
&.btn-disabled {
background: #e0e0e0;
color: #bababa;
&:hover {
background: #e0e0e0;
color: #bababa;
}
}
&.btn-blue {
background: $blue;
color: #8b8b8b;
&.null {
background: $grey-90;
color: $grey-50;
&:hover {
background: #4B9BE5;
color: #233E56;
}
&:hover {
background: $grey-70;
color: $grey-00;
}
}
&.btn-red {
background: #fa4242;
color: #860f0f;
&.wind {
background: $wind-bg-light;
color: $wind-text-dark;
&:hover {
background: #e91a1a;
color: #4e1717;
.icon {
color: #4e1717;
}
}
.icon {
color: #860f0f;
}
&:hover {
background: darken($wind-bg-light, 10);
}
}
&.btn-disabled {
background: #e0e0e0;
color: #bababa;
&.fire {
background: $fire-bg-light;
color: $fire-text-dark;
&:hover {
background: #e0e0e0;
color: #bababa;
}
&:hover {
background: darken($fire-bg-light, 10);
}
}
&.null {
background: $grey-90;
color: $grey-50;
&.water {
background: $water-bg-light;
color: $water-text-dark;
&:hover {
background: $grey-70;
color: $grey-00;
}
&:hover {
background: darken($water-bg-light, 10);
}
}
&.wind {
background: $wind-bg-light;
color: $wind-text-dark;
&.earth {
background: $earth-bg-light;
color: $earth-text-dark;
&:hover {
background: darken($wind-bg-light, 10);
}
&:hover {
background: darken($earth-bg-light, 10);
}
}
&.fire {
background: $fire-bg-light;
color: $fire-text-dark;
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
&:hover {
background: darken($fire-bg-light, 10);
}
&:hover {
background: darken($dark-bg-light, 10);
}
}
&.water {
background: $water-bg-light;
color: $water-text-dark;
&.light {
background: $light-bg-light;
color: $light-text-dark;
&:hover {
background: darken($water-bg-light, 10);
}
&:hover {
background: darken($light-bg-light, 10);
}
}
&.earth {
background: $earth-bg-light;
color: $earth-text-dark;
&:hover {
background: darken($earth-bg-light, 10);
}
}
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
&:hover {
background: darken($dark-bg-light, 10);
}
}
&.light {
background: $light-bg-light;
color: $light-text-dark;
&:hover {
background: darken($light-bg-light, 10);
}
}
.text {
color: inherit;
display: block;
width: 100%;
}
.text {
color: inherit;
display: block;
width: 100%;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,92 +1,125 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import React from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import * as HoverCard from '@radix-ui/react-hover-card'
import * as HoverCard from "@radix-ui/react-hover-card";
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from "~components/UncapIndicator";
import './index.scss'
import "./index.scss";
interface Props {
gridCharacter: GridCharacter
children: React.ReactNode
gridCharacter: GridCharacter;
children: React.ReactNode;
}
interface KeyNames {
[key: string]: {
en: string,
jp: string
}
[key: string]: {
en: string;
jp: string;
};
}
const CharacterHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const router = useRouter();
const { t } = useTranslation("common");
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Proficiency = [
"none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
const tintElement = Element[props.gridCharacter.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(' ', '_')}`
const tintElement = Element[props.gridCharacter.object.element];
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(
" ",
"_"
)}`;
function characterImage() {
let imgSrc = ""
function characterImage() {
let imgSrc = "";
if (props.gridCharacter) {
const character = props.gridCharacter.object
if (props.gridCharacter) {
const character = props.gridCharacter.object;
// Change the image based on the uncap level
let suffix = '01'
if (props.gridCharacter.uncap_level == 6)
suffix = '04'
else if (props.gridCharacter.uncap_level == 5)
suffix = '03'
else if (props.gridCharacter.uncap_level > 2)
suffix = '02'
// 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";
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`
}
return imgSrc
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_${suffix}.jpg`;
}
return (
<HoverCard.Root>
<HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard">
<div className="top">
<div className="title">
<h4>{ props.gridCharacter.object.name[locale] }</h4>
<img alt={props.gridCharacter.object.name[locale]} src={characterImage()} />
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon labelType={Element[props.gridCharacter.object.element]} />
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency1] } />
{ (props.gridCharacter.object.proficiency.proficiency2) ?
<WeaponLabelIcon labelType={ Proficiency[props.gridCharacter.object.proficiency.proficiency2] } />
: ''}
</div>
<UncapIndicator
type="character"
ulb={props.gridCharacter.object.uncap.ulb || false}
flb={props.gridCharacter.object.uncap.flb || false}
special={false}
/>
</div>
</div>
return imgSrc;
}
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
)
}
return (
<HoverCard.Root>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard">
<div className="top">
<div className="title">
<h4>{props.gridCharacter.object.name[locale]}</h4>
<img
alt={props.gridCharacter.object.name[locale]}
src={characterImage()}
/>
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon
labelType={Element[props.gridCharacter.object.element]}
/>
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency1
]
}
/>
{props.gridCharacter.object.proficiency.proficiency2 ? (
<WeaponLabelIcon
labelType={
Proficiency[
props.gridCharacter.object.proficiency.proficiency2
]
}
/>
) : (
""
)}
</div>
<UncapIndicator
type="character"
ulb={props.gridCharacter.object.uncap.ulb || false}
flb={props.gridCharacter.object.uncap.flb || false}
special={false}
/>
</div>
</div>
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 {
border-radius: 6px;
display: flex;
gap: $unit;
padding: $unit * 1.5;
&:hover {
background: $grey-90;
cursor: pointer;
}
img {
background: $grey-80;
border-radius: 6px;
display: inline-block;
height: 72px;
width: 120px;
}
.Info {
display: flex;
gap: $unit;
padding: $unit * 1.5;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
&:hover {
background: $grey-90;
cursor: pointer;
h5 {
color: #555;
display: inline-block;
font-size: $font-medium;
font-weight: $medium;
}
img {
background: $grey-80;
border-radius: 6px;
display: inline-block;
height: 72px;
width: 120px;
.UncapIndicator {
justify-content: left;
pointer-events: none;
}
.Info {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
.stars {
display: inline-block;
color: #ffa15e;
font-size: $font-xlarge;
h5 {
color: #555;
display: inline-block;
font-size: $font-medium;
font-weight: $medium;
}
.UncapIndicator {
justify-content: left;
pointer-events: none;
}
.stars {
display: inline-block;
color: #FFA15E;
font-size: $font-xlarge;
& > span {
color: #65DAFF;
}
}
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height/ $aspect-ratio);
}
}
& > span {
color: #65daff;
}
}
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height/ $aspect-ratio);
}
}
}
}

View file

@ -1,51 +1,54 @@
import React from 'react'
import { useRouter } from 'next/router'
import React from "react";
import { useRouter } from "next/router";
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import './index.scss'
import "./index.scss";
interface Props {
data: Character
onClick: () => void
data: Character;
onClick: () => void;
}
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const CharacterResult = (props: Props) => {
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const router = useRouter();
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const character = props.data
const character = props.data;
const characterUrl = () => {
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`
const characterUrl = () => {
let url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01.jpg`;
if (character.granblue_id === '3030182000') {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`
}
return url
if (character.granblue_id === "3030182000") {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-grid/${character.granblue_id}_01_01.jpg`;
}
return (
<li className="CharacterResult" onClick={props.onClick}>
<img alt={character.name[locale]} src={characterUrl()} />
<div className="Info">
<h5>{character.name[locale]}</h5>
<UncapIndicator
type="character"
flb={character.uncap.flb}
ulb={character.uncap.ulb}
special={character.special}
/>
<div className="tags">
<WeaponLabelIcon labelType={Element[character.element]} />
</div>
</div>
</li>
)
}
return url;
};
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 { useTranslation } from 'next-i18next'
import React, { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import cloneDeep from 'lodash.clonedeep'
import cloneDeep from "lodash.clonedeep";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import SearchFilter from "~components/SearchFilter";
import SearchFilterCheckboxItem from "~components/SearchFilterCheckboxItem";
import './index.scss'
import { emptyElementState, emptyProficiencyState, emptyRarityState } from '~utils/emptyStates'
import { elements, proficiencies, rarities } from '~utils/stateValues'
import "./index.scss";
import {
emptyElementState,
emptyProficiencyState,
emptyRarityState,
} from "~utils/emptyStates";
import { elements, proficiencies, rarities } from "~utils/stateValues";
interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void
sendFilters: (filters: { [key: string]: number[] }) => void;
}
const CharacterSearchFilterBar = (props: Props) => {
const { t } = useTranslation('common')
const { t } = useTranslation("common");
const [rarityMenu, setRarityMenu] = useState(false)
const [elementMenu, setElementMenu] = useState(false)
const [proficiency1Menu, setProficiency1Menu] = useState(false)
const [proficiency2Menu, setProficiency2Menu] = useState(false)
const [rarityMenu, setRarityMenu] = useState(false);
const [elementMenu, setElementMenu] = useState(false);
const [proficiency1Menu, setProficiency1Menu] = useState(false);
const [proficiency2Menu, setProficiency2Menu] = useState(false);
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(emptyProficiencyState)
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(emptyProficiencyState)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState);
const [elementState, setElementState] =
useState<ElementState>(emptyElementState);
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(
emptyProficiencyState
);
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(
emptyProficiencyState
);
function rarityMenuOpened(open: boolean) {
if (open) {
setRarityMenu(true)
setElementMenu(false)
setProficiency1Menu(false)
setProficiency2Menu(false)
} else setRarityMenu(false)
}
function rarityMenuOpened(open: boolean) {
if (open) {
setRarityMenu(true);
setElementMenu(false);
setProficiency1Menu(false);
setProficiency2Menu(false);
} else setRarityMenu(false);
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(true)
setProficiency1Menu(false)
setProficiency2Menu(false)
} else setElementMenu(false)
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(true);
setProficiency1Menu(false);
setProficiency2Menu(false);
} else setElementMenu(false);
}
function proficiency1MenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(false)
setProficiency1Menu(true)
setProficiency2Menu(false)
} else setProficiency1Menu(false)
}
function proficiency1MenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(false);
setProficiency1Menu(true);
setProficiency2Menu(false);
} else setProficiency1Menu(false);
}
function proficiency2MenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(false)
setProficiency1Menu(false)
setProficiency2Menu(true)
} else setProficiency2Menu(false)
}
function proficiency2MenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(false);
setProficiency1Menu(false);
setProficiency2Menu(true);
} else setProficiency2Menu(false);
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState)
newRarityState[key].checked = checked
setRarityState(newRarityState)
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState);
newRarityState[key].checked = checked;
setRarityState(newRarityState);
}
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState)
newElementState[key].checked = checked
setElementState(newElementState)
}
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState);
newElementState[key].checked = checked;
setElementState(newElementState);
}
function handleProficiency1Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency1State)
newProficiencyState[key].checked = checked
setProficiency1State(newProficiencyState)
}
function handleProficiency1Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency1State);
newProficiencyState[key].checked = checked;
setProficiency1State(newProficiencyState);
}
function handleProficiency2Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency2State)
newProficiencyState[key].checked = checked
setProficiency2State(newProficiencyState)
}
function handleProficiency2Change(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiency2State);
newProficiencyState[key].checked = checked;
setProficiency2State(newProficiencyState);
}
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
const checkedProficiency1Filters = Object.values(proficiency1State).filter(x => x.checked).map((x, i) => x.id)
const checkedProficiency2Filters = Object.values(proficiency2State).filter(x => x.checked).map((x, i) => x.id)
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState)
.filter((x) => x.checked)
.map((x, i) => x.id);
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id);
const checkedProficiency1Filters = Object.values(proficiency1State)
.filter((x) => x.checked)
.map((x, i) => x.id);
const checkedProficiency2Filters = Object.values(proficiency2State)
.filter((x) => x.checked)
.map((x, i) => x.id);
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiency1Filters,
proficiency2: checkedProficiency2Filters
}
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiency1Filters,
proficiency2: checkedProficiency2Filters,
};
props.sendFilters(filters)
}
props.sendFilters(filters);
}
useEffect(() => {
sendFilters()
}, [rarityState, elementState, proficiency1State, proficiency2State])
useEffect(() => {
sendFilters();
}, [rarityState, elementState, proficiency1State, proficiency2State]);
function renderProficiencyFilter(proficiency: 1 | 2) {
const onCheckedChange = (proficiency == 1) ? handleProficiency1Change : handleProficiency2Change
const numSelected = (proficiency == 1)
? Object.values(proficiency1State).map(x => x.checked).filter(Boolean).length
: Object.values(proficiency2State).map(x => x.checked).filter(Boolean).length
const open = (proficiency == 1) ? proficiency1Menu : proficiency2Menu
const onOpenChange = (proficiency == 1) ? proficiency1MenuOpened : proficiency2MenuOpened
return (
<SearchFilter
label={`${t('filters.labels.proficiency')} ${proficiency}`}
numSelected={numSelected}
open={open}
onOpenChange={onOpenChange}>
<DropdownMenu.Label className="Label">{`${t('filters.labels.proficiency')} ${proficiency}`}</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked = (proficiency == 1)
? proficiency1State[proficiencies[i]].checked
: proficiency2State[proficiencies[i]].checked
return (
<SearchFilterCheckboxItem
key={proficiencies[i]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i]}>
{t(`proficiencies.${proficiencies[i]}`)}
</SearchFilterCheckboxItem>
)}
) }
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked = (proficiency == 1)
? proficiency1State[proficiencies[i + (proficiencies.length / 2)]].checked
: proficiency2State[proficiencies[i + (proficiencies.length / 2)]].checked
return (
<SearchFilterCheckboxItem
key={proficiencies[i + (proficiencies.length / 2)]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i + (proficiencies.length / 2)]}>
{t(`proficiencies.${proficiencies[i + (proficiencies.length / 2)]}`)}
</SearchFilterCheckboxItem>
)}
) }
</DropdownMenu.Group>
</section>
</SearchFilter>
)
}
function renderProficiencyFilter(proficiency: 1 | 2) {
const onCheckedChange =
proficiency == 1 ? handleProficiency1Change : handleProficiency2Change;
const numSelected =
proficiency == 1
? Object.values(proficiency1State)
.map((x) => x.checked)
.filter(Boolean).length
: Object.values(proficiency2State)
.map((x) => x.checked)
.filter(Boolean).length;
const open = proficiency == 1 ? proficiency1Menu : proficiency2Menu;
const onOpenChange =
proficiency == 1 ? proficiency1MenuOpened : proficiency2MenuOpened;
return (
<div className="SearchFilterBar">
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}>
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label>
{ Array.from(Array(rarities.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
key={rarities[i]}
onCheckedChange={handleRarityChange}
checked={rarityState[rarities[i]].checked}
valueKey={rarities[i]}>
{t(`rarities.${rarities[i]}`)}
</SearchFilterCheckboxItem>
)}
) }
</SearchFilter>
<SearchFilter
label={`${t("filters.labels.proficiency")} ${proficiency}`}
numSelected={numSelected}
open={open}
onOpenChange={onOpenChange}
>
<DropdownMenu.Label className="Label">{`${t(
"filters.labels.proficiency"
)} ${proficiency}`}</DropdownMenu.Label>
<section>
<DropdownMenu.Group className="Group">
{Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked =
proficiency == 1
? proficiency1State[proficiencies[i]].checked
: proficiency2State[proficiencies[i]].checked;
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}>
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label>
{ Array.from(Array(elements.length)).map((x, i) => {
return (
<SearchFilterCheckboxItem
key={elements[i]}
onCheckedChange={handleElementChange}
checked={elementState[elements[i]].checked}
valueKey={elements[i]}>
{t(`elements.${elements[i]}`)}
</SearchFilterCheckboxItem>
)}
) }
</SearchFilter>
return (
<SearchFilterCheckboxItem
key={proficiencies[i]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i]}
>
{t(`proficiencies.${proficiencies[i]}`)}
</SearchFilterCheckboxItem>
);
})}
</DropdownMenu.Group>
<DropdownMenu.Group className="Group">
{Array.from(Array(proficiencies.length / 2)).map((x, i) => {
const checked =
proficiency == 1
? proficiency1State[
proficiencies[i + proficiencies.length / 2]
].checked
: proficiency2State[
proficiencies[i + proficiencies.length / 2]
].checked;
{ renderProficiencyFilter(1) }
{ renderProficiencyFilter(2) }
</div>
)
}
return (
<SearchFilterCheckboxItem
key={proficiencies[i + proficiencies.length / 2]}
onCheckedChange={onCheckedChange}
checked={checked}
valueKey={proficiencies[i + proficiencies.length / 2]}
>
{t(
`proficiencies.${
proficiencies[i + proficiencies.length / 2]
}`
)}
</SearchFilterCheckboxItem>
);
})}
</DropdownMenu.Group>
</section>
</SearchFilter>
);
}
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 {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
min-height: 320px;
max-width: 200px;
margin-bottom: $unit * 4;
&.editable .CharacterImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-tall;
}
&.filled h3 {
display: block;
}
&.filled ul {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
min-height: 320px;
max-width: 200px;
margin-bottom: $unit * 4;
}
&.editable .CharacterImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-tall;
h3,
ul {
display: none;
}
h3 {
color: #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 {
display: block;
&:hover .icon svg {
color: $grey-40;
}
&.filled ul {
display: flex;
}
h3,
ul {
display: none;
}
h3 {
color: #333;
font-size: $font-regular;
font-weight: $normal;
line-height: 1.1;
margin: 0;
max-width: 131px;
text-align: center;
word-wrap: normal;
}
img {
position: relative;
width: 100%;
z-index: 2;
}
.CharacterImage {
aspect-ratio: 131 / 273;
background: white;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.18s ease-in-out;
height: auto;
width: 131px;
@media (max-width: $medium-screen) {
width: 17vw;
}
&:hover .icon svg {
color: $grey-40;
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
svg {
fill: $grey-70;
}
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
svg {
fill: $grey-70;
}
}
}
}

View file

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

View file

@ -1,64 +1,65 @@
.ToggleGroup {
$height: 36px;
$height: 36px;
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: $height;
display: flex;
height: $height;
gap: calc($unit / 4);
padding: calc($unit / 2);
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: $height;
display: flex;
height: $height;
gap: calc($unit / 4);
padding: calc($unit / 2);
.ToggleItem {
background: white;
border: none;
border-radius: 18px;
color: $grey-40;
flex-grow: 1;
font-size: $font-regular;
padding: ($unit) $unit * 2;
.ToggleItem {
background: white;
border: none;
border-radius: 18px;
color: $grey-40;
flex-grow: 1;
font-size: $font-regular;
padding: ($unit) $unit * 2;
&.ja {
padding-top: 6px;
padding-bottom: 10px;
}
&:hover {
cursor: pointer;
}
&:hover, &[data-state="on"] {
background:$grey-80;
color: $grey-00;
&.fire {
background: $fire-bg-light;
color: $fire-text-dark;
}
&.water {
background: $water-bg-light;
color: $water-text-dark;
}
&.earth {
background: $earth-bg-light;
color: $earth-text-dark;
}
&.wind {
background: $wind-bg-light;
color: $wind-text-dark;
}
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
}
&.light {
background: $light-bg-light;
color: $light-text-dark;
}
}
&.ja {
padding-top: 6px;
padding-bottom: 10px;
}
&:hover {
cursor: pointer;
}
&:hover,
&[data-state="on"] {
background: $grey-80;
color: $grey-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 { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import React from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import * as ToggleGroup from '@radix-ui/react-toggle-group'
import * as ToggleGroup from "@radix-ui/react-toggle-group";
import './index.scss'
import "./index.scss";
interface Props {
currentElement: number
sendValue: (value: string) => void
currentElement: number;
sendValue: (value: string) => void;
}
const ElementToggle = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const router = useRouter();
const { t } = useTranslation("common");
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
return (
<ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}>
<ToggleGroup.Item className={`ToggleItem ${locale}`} value="0" aria-label="null">
{t('elements.null')}
</ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem wind ${locale}`} value="1" aria-label="wind">
{t('elements.wind')}
</ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem fire ${locale}`} value="2" aria-label="fire">
{t('elements.fire')}
</ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem water ${locale}`} value="3" aria-label="water">
{t('elements.water')}
</ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem earth ${locale}`} value="4" aria-label="earth">
{t('elements.earth')}
</ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem dark ${locale}`} value="5" aria-label="dark">
{t('elements.dark')}
</ToggleGroup.Item>
<ToggleGroup.Item className={`ToggleItem light ${locale}`} value="6" aria-label="light">
{t('elements.light')}
</ToggleGroup.Item>
</ToggleGroup.Root>
)
}
return (
<ToggleGroup.Root
className="ToggleGroup"
type="single"
defaultValue={`${props.currentElement}`}
aria-label="Element"
onValueChange={props.sendValue}
>
<ToggleGroup.Item
className={`ToggleItem ${locale}`}
value="0"
aria-label="null"
>
{t("elements.null")}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem wind ${locale}`}
value="1"
aria-label="wind"
>
{t("elements.wind")}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem fire ${locale}`}
value="2"
aria-label="fire"
>
{t("elements.fire")}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem water ${locale}`}
value="3"
aria-label="water"
>
{t("elements.water")}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem earth ${locale}`}
value="4"
aria-label="earth"
>
{t("elements.earth")}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem dark ${locale}`}
value="5"
aria-label="dark"
>
{t("elements.dark")}
</ToggleGroup.Item>
<ToggleGroup.Item
className={`ToggleItem light ${locale}`}
value="6"
aria-label="light"
>
{t("elements.light")}
</ToggleGroup.Item>
</ToggleGroup.Root>
);
};
export default ElementToggle
export default ElementToggle;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,63 +1,62 @@
.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;
background: white;
border-radius: 6px;
display: flex;
flex-direction: row;
gap: $unit * 2;
margin: 0 auto;
margin-top: 7px; // Line up with HeaderMenu
padding: $unit * 2;
position: sticky;
transition: box-shadow 0.24s ease-in-out;
top: $unit * 4;
width: 966px;
flex-grow: 1;
gap: $unit * 1.5;
&.shadow {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
}
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;
}
}
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 { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import React from "react";
import { useTranslation } from "next-i18next";
import classNames from "classnames";
import RaidDropdown from '~components/RaidDropdown'
import RaidDropdown from "~components/RaidDropdown";
import './index.scss'
import "./index.scss";
interface Props {
children: React.ReactNode
scrolled: boolean
element?: number
raidSlug?: string
recency?: number
onFilter: ({element, raidSlug, recency} : { element?: number, raidSlug?: string, recency?: number}) => void
children: React.ReactNode;
scrolled: boolean;
element?: number;
raidSlug?: string;
recency?: number;
onFilter: ({
element,
raidSlug,
recency,
}: {
element?: number;
raidSlug?: string;
recency?: number;
}) => void;
}
const FilterBar = (props: Props) => {
// Set up translation
const { t } = useTranslation('common')
// Set up translation
const { t } = useTranslation("common");
// Set up refs for filter dropdowns
const elementSelect = React.createRef<HTMLSelectElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const recencySelect = React.createRef<HTMLSelectElement>()
// Set up refs for filter dropdowns
const elementSelect = React.createRef<HTMLSelectElement>();
const raidSelect = React.createRef<HTMLSelectElement>();
const recencySelect = React.createRef<HTMLSelectElement>();
// Set up classes object for showing shadow on scroll
const classes = classNames({
'FilterBar': true,
'shadow': props.scrolled
})
// Set up classes object for showing shadow on scroll
const classes = classNames({
FilterBar: true,
shadow: props.scrolled,
});
function elementSelectChanged() {
const elementValue = (elementSelect.current) ? parseInt(elementSelect.current.value) : -1
props.onFilter({ element: elementValue })
}
function elementSelectChanged() {
const elementValue = elementSelect.current
? parseInt(elementSelect.current.value)
: -1;
props.onFilter({ element: elementValue });
}
function recencySelectChanged() {
const recencyValue = (recencySelect.current) ? parseInt(recencySelect.current.value) : -1
props.onFilter({ recency: recencyValue })
}
function recencySelectChanged() {
const recencyValue = recencySelect.current
? parseInt(recencySelect.current.value)
: -1;
props.onFilter({ recency: recencyValue });
}
function raidSelectChanged(slug?: string) {
props.onFilter({ raidSlug: slug })
}
function raidSelectChanged(slug?: string) {
props.onFilter({ raidSlug: slug });
}
return (
<div className={classes}>
{props.children}
<select onChange={elementSelectChanged} ref={elementSelect} value={props.element}>
<option data-element="all" key={-1} value={-1}>{t('elements.full.all')}</option>
<option data-element="null" key={0} value={0}>{t('elements.full.null')}</option>
<option data-element="wind" key={1} value={1}>{t('elements.full.wind')}</option>
<option data-element="fire" key={2} value={2}>{t('elements.full.fire')}</option>
<option data-element="water" key={3} value={3}>{t('elements.full.water')}</option>
<option data-element="earth" key={4} value={4}>{t('elements.full.earth')}</option>
<option data-element="dark" key={5} value={5}>{t('elements.full.dark')}</option>
<option data-element="light" key={6} value={6}>{t('elements.full.light')}</option>
</select>
<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>
)
}
return (
<div className={classes}>
{props.children}
<select
onChange={elementSelectChanged}
ref={elementSelect}
value={props.element}
>
<option data-element="all" key={-1} value={-1}>
{t("elements.full.all")}
</option>
<option data-element="null" key={0} value={0}>
{t("elements.full.null")}
</option>
<option data-element="wind" key={1} value={1}>
{t("elements.full.wind")}
</option>
<option data-element="fire" key={2} value={2}>
{t("elements.full.fire")}
</option>
<option data-element="water" key={3} value={3}>
{t("elements.full.water")}
</option>
<option data-element="earth" key={4} value={4}>
{t("elements.full.earth")}
</option>
<option data-element="dark" key={5} value={5}>
{t("elements.full.dark")}
</option>
<option data-element="light" key={6} value={6}>
{t("elements.full.light")}
</option>
</select>
<RaidDropdown
currentRaid={props.raidSlug}
showAllRaidsOption={true}
onChange={raidSelectChanged}
ref={raidSelect}
/>
<select onChange={recencySelectChanged} ref={recencySelect}>
<option key={-1} value={-1}>
{t("recency.all_time")}
</option>
<option key={86400} value={86400}>
{t("recency.last_day")}
</option>
<option key={604800} value={604800}>
{t("recency.last_week")}
</option>
<option key={2629746} value={2629746}>
{t("recency.last_month")}
</option>
<option key={7889238} value={7889238}>
{t("recency.last_3_months")}
</option>
<option key={15778476} value={15778476}>
{t("recency.last_6_months")}
</option>
<option key={31556952} value={31556952}>
{t("recency.last_year")}
</option>
</select>
</div>
);
};
export default FilterBar
export default FilterBar;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,153 +1,150 @@
.PartyDetails {
display: none; // This breaks transition, find a workaround
opacity: 0;
margin: 0 auto;
margin-bottom: 100px;
max-width: $unit * 95;
position: relative;
display: none; // This breaks transition, find a workaround
opacity: 0;
margin: 0 auto;
margin-bottom: 100px;
max-width: $unit * 95;
position: relative;
&.Editable {
top: $unit;
height: 0;
z-index: 2;
transition: opacity 0.2s ease-in-out,
top 0.2s ease-in-out;
&.Editable {
top: $unit;
height: 0;
z-index: 2;
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
&.Visible {
display: block;
height: auto;
margin-bottom: 40vh;
opacity: 1;
top: 0;
}
fieldset {
display: block;
width: 100%;
textarea {
min-height: $unit * 20;
width: 100%;
}
}
.bottom {
display: flex;
flex-direction: row;
gap: $unit;
.left {
flex-grow: 1;
}
.right {
display: flex;
flex-direction: row;
gap: $unit;
}
}
&.Visible {
display: block;
height: auto;
margin-bottom: 40vh;
opacity: 1;
top: 0;
}
&.ReadOnly {
top: $unit * -1;
transition: opacity 0.2s ease-in-out,
top 0.2s ease-in-out;
fieldset {
display: block;
width: 100%;
&.Visible {
display: block;
height: auto;
opacity: 1;
top: 0;
}
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;
}
}
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 {
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 {
display: none;
justify-content: center;
margin-bottom: $unit * 10;
display: none;
justify-content: center;
margin-bottom: $unit * 10;
&.Visible {
display: flex;
}
&.Visible {
display: flex;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,74 +1,74 @@
.DropdownLabel {
align-items: center;
background: $grey-90;
border: none;
border-radius: $unit * 2;
color: $grey-40;
display: flex;
gap: calc($unit / 2);
flex-direction: row;
padding: ($unit) ($unit * 2);
align-items: center;
background: $grey-90;
border: none;
border-radius: $unit * 2;
color: $grey-40;
display: flex;
gap: calc($unit / 2);
flex-direction: row;
padding: ($unit) ($unit * 2);
&:hover {
background: $grey-80;
color: $grey-00;
cursor: pointer;
}
.count {
color: $grey-60;
font-weight: $medium;
}
& > .icon {
$diameter: 12px;
height: $diameter;
width: $diameter;
svg {
transform: scale(0.85);
path {
fill: $grey-60;
}
}
&:hover {
background: $grey-80;
color: $grey-00;
cursor: pointer;
}
.count {
color: $grey-60;
font-weight: $medium;
}
& > .icon {
$diameter: 12px;
height: $diameter;
width: $diameter;
svg {
transform: scale(0.85);
path {
fill: $grey-60;
}
}
}
}
.Dropdown {
background: white;
border-radius: $unit;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
background: white;
border-radius: $unit;
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;
flex-direction: row;
gap: $unit;
}
.Group {
flex: 1 1 0px;
flex-direction: column;
gap: calc($unit / 2);
padding: $unit;
min-width: 120px;
}
& > span {
overflow: hidden;
svg {
fill: white;
filter: drop-shadow(0px 0px 1px rgb(0 0 0 / 0.18));
}
}
section {
display: flex;
flex-direction: row;
gap: $unit;
}
.Group {
flex: 1 1 0px;
flex-direction: column;
}
.Label {
color: $grey-60;
font-size: $font-small;
margin-bottom: calc($unit / 2);
padding-left: calc($unit / 2);
}
.Label {
color: $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 './index.scss'
import ArrowIcon from "~public/icons/Arrow.svg";
import "./index.scss";
interface Props {
label: string
open: boolean
numSelected: number
onOpenChange: (open: boolean) => void
children: React.ReactNode
label: string;
open: boolean;
numSelected: number;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
}
const SearchFilter = (props: Props) => {
return (
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
<DropdownMenu.Trigger className="DropdownLabel">
{props.label}
<span className="count">{props.numSelected}</span>
<span className="icon">
<ArrowIcon />
</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
{props.children}
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
return (
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
<DropdownMenu.Trigger className="DropdownLabel">
{props.label}
<span className="count">{props.numSelected}</span>
<span className="icon">
<ArrowIcon />
</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
{props.children}
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</DropdownMenu.Root>
);
};
export default SearchFilter
export default SearchFilter;

View file

@ -1,41 +1,41 @@
.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;
border-radius: calc($unit / 2);
color: $grey-40;
font-size: $font-regular;
line-height: 1.2;
min-width: 100px;
position: relative;
padding: $unit;
padding-left: $unit * 3;
justify-content: center;
position: absolute;
left: calc($unit / 2);
height: $diameter;
width: $diameter;
&:hover {
background: $grey-90;
cursor: pointer;
}
&[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;
}
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 './index.scss'
import CheckIcon from "~public/icons/Check.svg";
import "./index.scss";
interface Props {
checked?: boolean
valueKey: string
onCheckedChange: (open: boolean, key: string) => void
children: React.ReactNode
checked?: boolean;
valueKey: string;
onCheckedChange: (open: boolean, key: string) => void;
children: React.ReactNode;
}
const SearchFilterCheckboxItem = (props: Props) => {
function handleCheckedChange(checked: boolean) {
props.onCheckedChange(checked, props.valueKey)
}
function handleCheckedChange(checked: boolean) {
props.onCheckedChange(checked, props.valueKey);
}
return (
<DropdownMenu.CheckboxItem
className="Item"
checked={props.checked || false}
onCheckedChange={handleCheckedChange}
onSelect={ (event) => event.preventDefault() }>
<DropdownMenu.ItemIndicator className="Indicator">
<CheckIcon />
</DropdownMenu.ItemIndicator>
{props.children}
</DropdownMenu.CheckboxItem>
)
}
return (
<DropdownMenu.CheckboxItem
className="Item"
checked={props.checked || false}
onCheckedChange={handleCheckedChange}
onSelect={(event) => event.preventDefault()}
>
<DropdownMenu.ItemIndicator className="Indicator">
<CheckIcon />
</DropdownMenu.ItemIndicator>
{props.children}
</DropdownMenu.CheckboxItem>
);
};
export default SearchFilterCheckboxItem
export default SearchFilterCheckboxItem;

View file

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

View file

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

View file

@ -1,36 +1,36 @@
.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;
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;
&:before {
background: #fff;
}
&: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 {
groupName: string
name: string
selected: boolean
children: string
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
groupName: string;
name: string;
selected: boolean;
children: string;
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const Segment: React.FC<Props> = (props: Props) => {
return (
<div className="Segment">
<input
name={props.groupName}
id={props.name}
value={props.name}
type="radio"
checked={props.selected}
onChange={props.onClick}
/>
<label htmlFor={props.name}>{props.children}</label>
</div>
);
};
return (
<div className="Segment">
<input
name={props.groupName}
id={props.name}
value={props.name}
type="radio"
checked={props.selected}
onChange={props.onClick}
/>
<label htmlFor={props.name}>
{props.children}
</label>
</div>
)
}
export default Segment
export default Segment;

View file

@ -1,88 +1,88 @@
.SegmentedControlWrapper {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
.SegmentedControl {
background: white;
border-radius: $unit * 3;
display: inline-flex;
padding: 3px;
position: relative;
user-select: none;
overflow: hidden;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
z-index: 1;
background: white;
border-radius: $unit * 3;
display: inline-flex;
padding: 3px;
position: relative;
user-select: none;
overflow: hidden;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
z-index: 1;
&.fire {
.Segment input:checked + label {
background: $fire-bg-dark;
color: $fire-text-dark;
}
.Segment:hover label {
background: $fire-bg-light;
color: $fire-text-light;
}
&.fire {
.Segment input:checked + label {
background: $fire-bg-dark;
color: $fire-text-dark;
}
&.water {
.Segment input:checked + label {
background: $water-bg-dark;
color: $water-text-dark;
}
.Segment:hover label {
background: $fire-bg-light;
color: $fire-text-light;
}
}
.Segment:hover label {
background: $water-bg-light;
color: $water-text-light;
}
&.water {
.Segment input:checked + label {
background: $water-bg-dark;
color: $water-text-dark;
}
&.earth {
.Segment input:checked + label {
background: $earth-bg-dark;
color: $earth-text-dark;
}
.Segment:hover label {
background: $water-bg-light;
color: $water-text-light;
}
}
.Segment:hover label {
background: $earth-bg-light;
color: $earth-text-light;
}
&.earth {
.Segment input:checked + label {
background: $earth-bg-dark;
color: $earth-text-dark;
}
&.wind {
.Segment input:checked + label {
background: $wind-bg-dark;
color: $wind-text-dark;
}
.Segment:hover label {
background: $earth-bg-light;
color: $earth-text-light;
}
}
.Segment:hover label {
background: $wind-bg-light;
color: $wind-text-light;
}
&.wind {
.Segment input:checked + label {
background: $wind-bg-dark;
color: $wind-text-dark;
}
&.light {
.Segment input:checked + label {
background: $light-bg-dark;
color: $light-text-dark;
}
.Segment:hover label {
background: $wind-bg-light;
color: $wind-text-light;
}
}
.Segment:hover label {
background: $light-bg-light;
color: $light-text-light;
}
&.light {
.Segment input:checked + label {
background: $light-bg-dark;
color: $light-text-dark;
}
&.dark {
.Segment input:checked + label {
background: $dark-bg-dark;
color: $dark-text-dark;
}
.Segment:hover label {
background: $dark-bg-light;
color: $dark-text-light;
}
.Segment:hover label {
background: $light-bg-light;
color: $light-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 {
elementClass?: string
elementClass?: string;
}
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
return (
<div className="SegmentedControlWrapper">
<div className={`SegmentedControl ${(elementClass) ? elementClass : ''}`}>
{children}
</div>
</div>
)
}
return (
<div className="SegmentedControlWrapper">
<div className={`SegmentedControl ${elementClass ? elementClass : ""}`}>
{children}
</div>
</div>
);
};
export default SegmentedControl
export default SegmentedControl;

View file

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

View file

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

View file

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

View file

@ -1,80 +1,99 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import React from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import * as HoverCard from '@radix-ui/react-hover-card'
import * as HoverCard from "@radix-ui/react-hover-card";
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import UncapIndicator from "~components/UncapIndicator";
import './index.scss'
import "./index.scss";
interface Props {
gridSummon: GridSummon
children: React.ReactNode
gridSummon: GridSummon;
children: React.ReactNode;
}
const SummonHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const router = useRouter();
const { t } = useTranslation("common");
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const tintElement = Element[props.gridSummon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(' ', '_')}`
const tintElement = Element[props.gridSummon.object.element];
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(
" ",
"_"
)}`;
function summonImage() {
let imgSrc = ""
function summonImage() {
let imgSrc = "";
if (props.gridSummon) {
const summon = props.gridSummon.object
if (props.gridSummon) {
const summon = props.gridSummon.object;
const upgradedSummons = [
'2040094000', '2040100000', '2040080000', '2040098000',
'2040090000', '2040084000', '2040003000', '2040056000'
]
const upgradedSummons = [
"2040094000",
"2040100000",
"2040080000",
"2040098000",
"2040090000",
"2040084000",
"2040003000",
"2040056000",
];
let suffix = ''
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5)
suffix = '_02'
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 imgSrc
// Generate the correct source for the summon
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`;
}
return (
<HoverCard.Root>
<HoverCard.Trigger>
{ props.children }
</HoverCard.Trigger>
<HoverCard.Content className="Weapon Hovercard">
<div className="top">
<div className="title">
<h4>{ props.gridSummon.object.name[locale] }</h4>
<img alt={props.gridSummon.object.name[locale]} src={summonImage()} />
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon labelType={Element[props.gridSummon.object.element]}/>
</div>
<UncapIndicator
type="summon"
ulb={props.gridSummon.object.uncap.ulb || false}
flb={props.gridSummon.object.uncap.flb || false}
special={false}
/>
</div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">{t('buttons.wiki')}</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
)
}
return imgSrc;
}
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 {
border-radius: 6px;
display: flex;
gap: $unit;
padding: $unit * 1.5;
&:hover {
background: $grey-90;
cursor: pointer;
}
img {
background: $grey-80;
border-radius: 6px;
display: inline-block;
height: auto;
width: 120px;
}
.Info {
display: flex;
gap: $unit;
padding: $unit * 1.5;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
&:hover {
background: $grey-90;
cursor: pointer;
h5 {
color: #555;
display: inline-block;
font-size: $font-medium;
font-weight: $medium;
}
img {
background: $grey-80;
border-radius: 6px;
display: inline-block;
height: auto;
width: 120px;
.UncapIndicator {
justify-content: left;
pointer-events: none;
}
.Info {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: calc($unit / 2);
.stars {
display: inline-block;
color: #ffa15e;
font-size: $font-xlarge;
h5 {
color: #555;
display: inline-block;
font-size: $font-medium;
font-weight: $medium;
}
.UncapIndicator {
justify-content: left;
pointer-events: none;
}
.stars {
display: inline-block;
color: #FFA15E;
font-size: $font-xlarge;
& > span {
color: #65DAFF;
}
}
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height / $aspect-ratio);
}
}
& > span {
color: #65daff;
}
}
.tags {
display: flex;
flex-direction: row;
gap: calc($unit / 2);
.WeaponLabelIcon {
$aspect-ratio: calc(25 / 60);
$height: 22px;
background-size: calc($height / $aspect-ratio) $height;
background-repeat: no-repeat;
height: $height;
width: calc($height / $aspect-ratio);
}
}
}
}

View file

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

View file

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

View file

@ -1,106 +1,107 @@
.SummonUnit {
display: flex;
flex-direction: column;
gap: 4px;
display: flex;
flex-direction: column;
gap: 4px;
&.main .SummonImage,
&.friend .SummonImage {
aspect-ratio: 182 / 315;
width: 182px;
height: auto;
&.main .SummonImage,
&.friend .SummonImage {
aspect-ratio: 182 / 315;
width: 182px;
height: auto;
@media (max-width: $medium-screen) {
width: 20.3vw;
}
@media (max-width: $medium-screen) {
width: 20.3vw;
}
}
&.grid {
// max-width: 148px;
// min-height: 141px;
min-height: 180px;
&.grid {
// max-width: 148px;
// min-height: 141px;
min-height: 180px;
@media (max-width: $medium-screen) {
min-height: 16.5vw;
}
.SummonImage {
aspect-ratio: 148 / 111;
list-style-type: none;
width: 148px;
height: auto;
@media (max-width: $medium-screen) {
width: 20vw;
}
}
}
&.friend {
margin-right: 0;
}
&.main.editable .SummonImage:hover,
&.friend.editable .SummonImage:hover {
transform: $scale-tall;
}
&.editable .SummonImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
@media (max-width: $medium-screen) {
min-height: 16.5vw;
}
.SummonImage {
background: white;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.18s ease-in-out;
aspect-ratio: 148 / 111;
list-style-type: none;
width: 148px;
height: auto;
&:hover .icon svg {
fill: $grey-40;
}
@media (max-width: $medium-screen) {
width: 20vw;
}
}
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
&.friend {
margin-right: 0;
}
svg {
fill: $grey-70;
}
}
&.main.editable .SummonImage:hover,
&.friend.editable .SummonImage:hover {
transform: $scale-tall;
}
&.editable .SummonImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
}
.SummonImage {
background: 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 {
display: block;
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
&.filled ul {
display: flex;
svg {
fill: $grey-70;
}
}
}
h3, ul {
display: none;
}
&.filled h3 {
display: block;
}
h3 {
color: #333;
font-size: $font-regular;
font-weight: $normal;
line-height: 1.1;
margin: 0;
text-align: center;
}
&.filled ul {
display: flex;
}
img {
position: relative;
width: 100%;
z-index: 2;
}
h3,
ul {
display: none;
}
h3 {
color: #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 { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
import classnames from "classnames"
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import classnames from "classnames";
import SearchModal from "~components/SearchModal"
import SummonHovercard from "~components/SummonHovercard"
import UncapIndicator from "~components/UncapIndicator"
import PlusIcon from "~public/icons/Add.svg"
import SearchModal from "~components/SearchModal";
import SummonHovercard from "~components/SummonHovercard";
import UncapIndicator from "~components/UncapIndicator";
import PlusIcon from "~public/icons/Add.svg";
import type { SearchableObject } from "~types"
import type { SearchableObject } from "~types";
import "./index.scss"
import "./index.scss";
interface Props {
gridSummon: GridSummon | undefined
unitType: 0 | 1 | 2
position: number
editable: boolean
updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
gridSummon: GridSummon | undefined;
unitType: 0 | 1 | 2;
position: number;
editable: boolean;
updateObject: (object: SearchableObject, position: number) => void;
updateUncap: (id: string, position: number, uncap: number) => void;
}
const SummonUnit = (props: Props) => {
const { t } = useTranslation("common")
const { t } = useTranslation("common");
const [imageUrl, setImageUrl] = useState("")
const [imageUrl, setImageUrl] = useState("");
const router = useRouter()
const router = useRouter();
const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const classes = classnames({
SummonUnit: true,
@ -37,19 +39,19 @@ const SummonUnit = (props: Props) => {
friend: props.unitType == 2,
editable: props.editable,
filled: props.gridSummon !== undefined,
})
});
const gridSummon = props.gridSummon
const summon = gridSummon?.object
const gridSummon = props.gridSummon;
const summon = gridSummon?.object;
useEffect(() => {
generateImageUrl()
})
generateImageUrl();
});
function generateImageUrl() {
let imgSrc = ""
let imgSrc = "";
if (props.gridSummon) {
const summon = props.gridSummon.object!
const summon = props.gridSummon.object!;
const upgradedSummons = [
"2040094000",
@ -66,28 +68,28 @@ const SummonUnit = (props: Props) => {
"2040027000",
"2040046000",
"2040047000",
]
];
let suffix = ""
let suffix = "";
if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5
)
suffix = "_02"
suffix = "_02";
// Generate the correct source for the summon
if (props.unitType == 0 || props.unitType == 2)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`;
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`;
}
setImageUrl(imgSrc)
setImageUrl(imgSrc);
}
function passUncapData(uncap: number) {
if (props.gridSummon)
props.updateUncap(props.gridSummon.id, props.position, uncap)
props.updateUncap(props.gridSummon.id, props.position, uncap);
}
const image = (
@ -101,7 +103,7 @@ const SummonUnit = (props: Props) => {
""
)}
</div>
)
);
const editableImage = (
<SearchModal
@ -112,7 +114,7 @@ const SummonUnit = (props: Props) => {
>
{image}
</SearchModal>
)
);
const unitContent = (
<div className={classes}>
@ -131,13 +133,13 @@ const SummonUnit = (props: Props) => {
)}
<h3 className="SummonName">{summon?.name[locale]}</h3>
</div>
)
);
const withHovercard = (
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
)
);
return gridSummon && !props.editable ? withHovercard : unitContent
}
return gridSummon && !props.editable ? withHovercard : unitContent;
};
export default SummonUnit
export default SummonUnit;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,44 +1,44 @@
.Weapon.Dialog {
.mods {
display: flex;
flex-direction: column;
gap: $unit * 4;
.mods {
display: flex;
flex-direction: column;
gap: $unit * 4;
section {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
section {
display: flex;
flex-direction: column;
gap: calc($unit / 2);
h3 {
color: $grey-50;
font-size: $font-small;
margin-bottom: $unit;
}
h3 {
color: $grey-50;
font-size: $font-small;
margin-bottom: $unit;
}
select {
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;
}
}
}
select {
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;
}
}
}
}
}

View file

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

View file

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

View file

@ -1,43 +1,87 @@
import React from 'react'
import { useRouter } from 'next/router'
import React from "react";
import { useRouter } from "next/router";
import UncapIndicator from '~components/UncapIndicator'
import WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from "~components/UncapIndicator";
import WeaponLabelIcon from "~components/WeaponLabelIcon";
import './index.scss'
import "./index.scss";
interface Props {
data: Weapon
onClick: () => void
data: Weapon;
onClick: () => void;
}
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = ['none', 'sword', 'dagger', 'axe', 'spear', 'bow', 'staff', 'fist', 'harp', 'gun', 'katana']
const Series = ['seraphic', 'grand', 'opus', 'draconic', 'revenant', 'primal', 'beast','regalia', 'omega', 'olden_primal', 'hollowsky', 'xeno', 'astral', 'rose', 'ultima', 'bahamut', 'epic', 'ennead', 'cosmos', 'ancestral', 'superlative', 'vintage', 'class_champion', 'sephira', 'new_world_foundation']
const Element = ["null", "wind", "fire", "water", "earth", "dark", "light"];
const Proficiency = [
"none",
"sword",
"dagger",
"axe",
"spear",
"bow",
"staff",
"fist",
"harp",
"gun",
"katana",
];
const Series = [
"seraphic",
"grand",
"opus",
"draconic",
"revenant",
"primal",
"beast",
"regalia",
"omega",
"olden_primal",
"hollowsky",
"xeno",
"astral",
"rose",
"ultima",
"bahamut",
"epic",
"ennead",
"cosmos",
"ancestral",
"superlative",
"vintage",
"class_champion",
"sephira",
"new_world_foundation",
];
const WeaponResult = (props: Props) => {
const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
const weapon = props.data
const router = useRouter();
const locale =
router.locale && ["en", "ja"].includes(router.locale)
? router.locale
: "en";
const weapon = props.data;
return (
<li className="WeaponResult" onClick={props.onClick}>
<img alt={weapon.name[locale]} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`} />
<div className="Info">
<h5>{weapon.name[locale]}</h5>
<UncapIndicator
type="weapon"
flb={weapon.uncap.flb}
ulb={weapon.uncap.ulb}
special={false}
/>
<div className="tags">
<WeaponLabelIcon labelType={Element[weapon.element]} />
<WeaponLabelIcon labelType={Proficiency[weapon.proficiency]} />
</div>
</div>
</li>
)
}
return (
<li className="WeaponResult" onClick={props.onClick}>
<img
alt={weapon.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`}
/>
<div className="Info">
<h5>{weapon.name[locale]}</h5>
<UncapIndicator
type="weapon"
flb={weapon.uncap.flb}
ulb={weapon.uncap.ulb}
special={false}
/>
<div className="tags">
<WeaponLabelIcon labelType={Element[weapon.element]} />
<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 { useTranslation } from 'next-i18next'
import React, { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import cloneDeep from 'lodash.clonedeep'
import cloneDeep from "lodash.clonedeep";
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import SearchFilter from '~components/SearchFilter'
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
import SearchFilter from "~components/SearchFilter";
import SearchFilterCheckboxItem from "~components/SearchFilterCheckboxItem";
import './index.scss'
import { emptyElementState, emptyProficiencyState, emptyRarityState, emptyWeaponSeriesState } from '~utils/emptyStates'
import { elements, proficiencies, rarities, weaponSeries } from '~utils/stateValues'
import "./index.scss";
import {
emptyElementState,
emptyProficiencyState,
emptyRarityState,
emptyWeaponSeriesState,
} from "~utils/emptyStates";
import {
elements,
proficiencies,
rarities,
weaponSeries,
} from "~utils/stateValues";
interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void
sendFilters: (filters: { [key: string]: number[] }) => void;
}
const WeaponSearchFilterBar = (props: Props) => {
const { t } = useTranslation('common')
const { t } = useTranslation("common");
const [rarityMenu, setRarityMenu] = useState(false)
const [elementMenu, setElementMenu] = useState(false)
const [proficiencyMenu, setProficiencyMenu] = useState(false)
const [seriesMenu, setSeriesMenu] = useState(false)
const [rarityMenu, setRarityMenu] = useState(false);
const [elementMenu, setElementMenu] = useState(false);
const [proficiencyMenu, setProficiencyMenu] = useState(false);
const [seriesMenu, setSeriesMenu] = useState(false);
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
const [proficiencyState, setProficiencyState] = useState<ProficiencyState>(emptyProficiencyState)
const [seriesState, setSeriesState] = useState<WeaponSeriesState>(emptyWeaponSeriesState)
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState);
const [elementState, setElementState] =
useState<ElementState>(emptyElementState);
const [proficiencyState, setProficiencyState] = useState<ProficiencyState>(
emptyProficiencyState
);
const [seriesState, setSeriesState] = useState<WeaponSeriesState>(
emptyWeaponSeriesState
);
function rarityMenuOpened(open: boolean) {
if (open) {
setRarityMenu(true)
setElementMenu(false)
setProficiencyMenu(false)
setSeriesMenu(false)
} else setRarityMenu(false)
}
function rarityMenuOpened(open: boolean) {
if (open) {
setRarityMenu(true);
setElementMenu(false);
setProficiencyMenu(false);
setSeriesMenu(false);
} else setRarityMenu(false);
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(true)
setProficiencyMenu(false)
setSeriesMenu(false)
} else setElementMenu(false)
}
function elementMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(true);
setProficiencyMenu(false);
setSeriesMenu(false);
} else setElementMenu(false);
}
function proficiencyMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(false)
setProficiencyMenu(true)
setSeriesMenu(false)
} else setProficiencyMenu(false)
}
function proficiencyMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(false);
setProficiencyMenu(true);
setSeriesMenu(false);
} else setProficiencyMenu(false);
}
function seriesMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false)
setElementMenu(false)
setProficiencyMenu(false)
setSeriesMenu(true)
} else setSeriesMenu(false)
}
function seriesMenuOpened(open: boolean) {
if (open) {
setRarityMenu(false);
setElementMenu(false);
setProficiencyMenu(false);
setSeriesMenu(true);
} else setSeriesMenu(false);
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState)
newRarityState[key].checked = checked
setRarityState(newRarityState)
}
function handleRarityChange(checked: boolean, key: string) {
let newRarityState = cloneDeep(rarityState);
newRarityState[key].checked = checked;
setRarityState(newRarityState);
}
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState)
newElementState[key].checked = checked
setElementState(newElementState)
}
function handleElementChange(checked: boolean, key: string) {
let newElementState = cloneDeep(elementState);
newElementState[key].checked = checked;
setElementState(newElementState);
}
function handleProficiencyChange(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiencyState)
newProficiencyState[key].checked = checked
setProficiencyState(newProficiencyState)
}
function handleProficiencyChange(checked: boolean, key: string) {
let newProficiencyState = cloneDeep(proficiencyState);
newProficiencyState[key].checked = checked;
setProficiencyState(newProficiencyState);
}
function handleSeriesChange(checked: boolean, key: string) {
let newSeriesState = cloneDeep(seriesState)
newSeriesState[key].checked = checked
setSeriesState(newSeriesState)
}
function handleSeriesChange(checked: boolean, key: string) {
let newSeriesState = cloneDeep(seriesState);
newSeriesState[key].checked = checked;
setSeriesState(newSeriesState);
}
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
const checkedProficiencyFilters = Object.values(proficiencyState).filter(x => x.checked).map((x, i) => x.id)
const checkedSeriesFilters = Object.values(seriesState).filter(x => x.checked).map((x, i) => x.id)
function sendFilters() {
const checkedRarityFilters = Object.values(rarityState)
.filter((x) => x.checked)
.map((x, i) => x.id);
const checkedElementFilters = Object.values(elementState)
.filter((x) => x.checked)
.map((x, i) => x.id);
const checkedProficiencyFilters = Object.values(proficiencyState)
.filter((x) => x.checked)
.map((x, i) => x.id);
const checkedSeriesFilters = Object.values(seriesState)
.filter((x) => x.checked)
.map((x, i) => x.id);
const filters = {
rarity: checkedRarityFilters,
element: checkedElementFilters,
proficiency1: checkedProficiencyFilters,
series: checkedSeriesFilters
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(() => {
sendFilters()
}, [rarityState, elementState, proficiencyState, seriesState])
<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>
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.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>
);
};
<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>
<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
export default WeaponSearchFilterBar;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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