Merge pull request #52 from jedmund/bug-fixes

Bug fixes!
This commit is contained in:
Justin Edmund 2022-12-26 05:17:41 -08:00 committed by GitHub
commit 3f24886b98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 677 additions and 361 deletions

View file

@ -80,6 +80,10 @@ const AccountModal = (props: Props) => {
// const [privateProfile, setPrivateProfile] = useState(false)
// Setup
const [pictureOpen, setPictureOpen] = useState(false)
const [genderOpen, setGenderOpen] = useState(false)
const [languageOpen, setLanguageOpen] = useState(false)
const [themeOpen, setThemeOpen] = useState(false)
// UI management
function openChange(open: boolean) {
@ -87,16 +91,10 @@ const AccountModal = (props: Props) => {
}
function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') {
const stateVars = selectOpenState
Object.keys(stateVars).forEach((key) => {
if (key === name) {
stateVars[name] = true
} else {
stateVars[key] = false
}
})
setSelectOpenState(stateVars)
setPictureOpen(name === 'picture' ? !pictureOpen : false)
setGenderOpen(name === 'gender' ? !genderOpen : false)
setLanguageOpen(name === 'language' ? !languageOpen : false)
setThemeOpen(name === 'theme' ? !themeOpen : false)
}
// Event handlers
@ -117,6 +115,14 @@ const AccountModal = (props: Props) => {
setAppTheme(value)
}
function onEscapeKeyDown(event: KeyboardEvent) {
if (pictureOpen || genderOpen || languageOpen || themeOpen) {
return event.preventDefault()
} else {
setOpen(false)
}
}
// API calls
function update(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
@ -189,9 +195,10 @@ const AccountModal = (props: Props) => {
description={t('modals.settings.descriptions.picture')}
className="Image"
label={t('modals.settings.labels.picture')}
open={selectOpenState.picture}
onClick={() => openSelect('picture')}
open={pictureOpen}
onOpenChange={() => openSelect('picture')}
onChange={handlePictureChange}
onClose={() => setPictureOpen(false)}
imageAlt={t('modals.settings.labels.image_alt')}
imageClass={pictureData.find((i) => i.filename === picture)?.element}
imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]}
@ -206,9 +213,10 @@ const AccountModal = (props: Props) => {
name="gender"
description={t('modals.settings.descriptions.gender')}
label={t('modals.settings.labels.gender')}
open={selectOpenState.gender}
onClick={() => openSelect('gender')}
open={genderOpen}
onOpenChange={() => openSelect('gender')}
onChange={handleGenderChange}
onClose={() => setGenderOpen(false)}
value={`${gender}`}
>
<SelectItem key="gran" value="0">
@ -224,9 +232,10 @@ const AccountModal = (props: Props) => {
<SelectTableField
name="language"
label={t('modals.settings.labels.language')}
open={selectOpenState.language}
onClick={() => openSelect('language')}
open={languageOpen}
onOpenChange={() => openSelect('language')}
onChange={handleLanguageChange}
onClose={() => setLanguageOpen(false)}
value={language}
>
<SelectItem key="en" value="en">
@ -242,9 +251,10 @@ const AccountModal = (props: Props) => {
<SelectTableField
name="theme"
label={t('modals.settings.labels.theme')}
open={selectOpenState.theme}
onClick={() => openSelect('theme')}
open={themeOpen}
onOpenChange={() => openSelect('theme')}
onChange={handleThemeChange}
onClose={() => setThemeOpen(false)}
value={theme}
>
<SelectItem key="system" value="system">
@ -274,7 +284,11 @@ const AccountModal = (props: Props) => {
<span>{t('menu.settings')}</span>
</li>
</DialogTrigger>
<DialogContent className="Account Dialog">
<DialogContent
className="Account Dialog"
onOpenAutoFocus={(event: Event) => {}}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader">
<div className="DialogTop">
<DialogTitle className="SubTitle">

View file

@ -17,6 +17,7 @@ interface Props {
object: 'character' | 'weapon'
awakeningType?: number
awakeningLevel?: number
onOpenChange: (open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (type: number, level: number) => void
}
@ -81,6 +82,11 @@ const AwakeningSelect = (props: Props) => {
// Classes
function changeOpen() {
setOpen(!open)
props.onOpenChange(!open)
}
function onClose() {
props.onOpenChange(false)
}
function generateOptions(object: 'character' | 'weapon') {
@ -165,7 +171,8 @@ const AwakeningSelect = (props: Props) => {
value={`${awakeningType}`}
open={open}
onValueChange={handleSelectChange}
onClick={() => changeOpen()}
onOpenChange={() => changeOpen()}
onClose={onClose}
triggerClass="modal"
>
{generateOptions(props.object)}

View file

@ -20,6 +20,7 @@ interface ErrorMap {
interface Props {
axType: number
currentSkills?: SimpleAxSkill[]
onOpenChange: (index: 1 | 2, open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (
primaryAxModifier: number,
@ -193,9 +194,22 @@ const AXSelect = (props: Props) => {
: undefined
}
function openSelect(ref: ForwardedRef<HTMLButtonElement>) {
if (ref === primaryAxModifierSelect) setOpenAX1(!openAX1)
if (ref === secondaryAxModifierSelect) setOpenAX2(!openAX2)
function openSelect(index: 1 | 2) {
if (index === 1) {
setOpenAX1(!openAX1)
setOpenAX2(false)
props.onOpenChange(1, !openAX1)
props.onOpenChange(2, false)
} else if (index === 2) {
setOpenAX2(!openAX2)
setOpenAX1(false)
props.onOpenChange(2, !openAX2)
props.onOpenChange(1, false)
}
}
function onClose(index: 1 | 2) {
props.onOpenChange(index, false)
}
function generateOptions(modifierSet: number) {
@ -392,8 +406,9 @@ const AXSelect = (props: Props) => {
key="ax1"
value={`${primaryAxModifier}`}
open={openAX1}
onClose={() => onClose(1)}
onOpenChange={() => openSelect(1)}
onValueChange={handleAX1SelectChange}
onClick={() => openSelect(primaryAxModifierSelect)}
triggerClass="modal"
>
{generateOptions(0)}
@ -419,8 +434,9 @@ const AXSelect = (props: Props) => {
key="ax2"
value={`${secondaryAxModifier}`}
open={openAX2}
onClose={() => onClose(2)}
onOpenChange={() => openSelect(2)}
onValueChange={handleAX2SelectChange}
onClick={() => openSelect(secondaryAxModifierSelect)}
triggerClass="modal"
ref={secondaryAxModifierSelect}
>

View file

@ -17,6 +17,10 @@
// box-shadow: 0 2px rgba(255, 255, 255, 1);
}
.Input {
padding: $unit * 1.5 $unit-2x;
}
.Counter {
color: $grey-55;
font-weight: $bold;

View file

@ -8,7 +8,10 @@ interface Props
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {}
> {
onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void
}
export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
function dialog({ children, ...props }, forwardedRef) {
@ -25,6 +28,8 @@ export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
<DialogPrimitive.Content
className={classes}
{...props}
onOpenAutoFocus={props.onOpenAutoFocus}
onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef}
>
{children}

View file

@ -60,12 +60,18 @@ const FilterBar = (props: Props) => {
props.onFilter({ raidSlug: slug })
}
function onSelectChange(name: 'element' | 'recency') {
setElementOpen(name === 'element' ? !elementOpen : false)
setRecencyOpen(name === 'recency' ? !recencyOpen : false)
}
return (
<div className={classes}>
{props.children}
<Select
value={`${props.element}`}
open={elementOpen}
onOpenChange={() => onSelectChange('element')}
onValueChange={elementSelectChanged}
onClick={openElementSelect}
>
@ -106,6 +112,7 @@ const FilterBar = (props: Props) => {
value={`${props.recency}`}
trigger={'All time'}
open={recencyOpen}
onOpenChange={() => onSelectChange('recency')}
onValueChange={recencySelectChanged}
onClick={openRecencySelect}
>

View file

@ -99,6 +99,10 @@
.bottom {
display: flex;
flex-direction: row;
a.user:hover {
color: var(--link-text-hover);
}
}
.raid,

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
@ -132,11 +133,28 @@ const GridRep = (props: Props) => {
} else return <div className="no-user" />
}
const linkedAttribution = () => (
<Link href={`/${props.user ? props.user.username : '#'}`}>
<a
className={userClass}
href={`/${props.user ? props.user.username : '#'}`}
>
{userImage()}
{props.user ? props.user.username : t('no_user')}
</a>
</Link>
)
const unlinkedAttribution = () => (
<div className={userClass}>
{userImage()}
{props.user ? props.user.username : t('no_user')}
</div>
)
const details = (
<div className="Details">
<h2 className={titleClass} onClick={navigate}>
{props.name ? props.name : t('no_title')}
</h2>
<h2 className={titleClass}>{props.name ? props.name : t('no_title')}</h2>
<div className="bottom">
<div className={raidClass}>
{props.raid ? props.raid.name[locale] : t('no_raid')}
@ -152,7 +170,7 @@ const GridRep = (props: Props) => {
<div className="Details">
<div className="top">
<div className="info">
<h2 className={titleClass} onClick={navigate}>
<h2 className={titleClass}>
{props.name ? props.name : t('no_title')}
</h2>
<div className={raidClass}>
@ -162,23 +180,25 @@ const GridRep = (props: Props) => {
{account.authorized &&
((props.user && account.user && account.user.id !== props.user.id) ||
!props.user) ? (
<Button
className="Save"
accessoryIcon={<SaveIcon className="stroke" />}
active={props.favorited}
contained={true}
buttonSize="small"
onClick={sendSaveData}
/>
<Link href="#">
<a href="#">
<Button
className="Save"
accessoryIcon={<SaveIcon className="stroke" />}
active={props.favorited}
contained={true}
buttonSize="small"
onClick={sendSaveData}
/>
</a>
</Link>
) : (
''
)}
</div>
<div className="bottom">
<div className={userClass}>
{userImage()}
{props.user ? props.user.username : t('no_user')}
</div>
{props.user ? linkedAttribution() : unlinkedAttribution()}
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
{formatTimeAgo(props.createdAt, locale)}
</time>
@ -187,25 +207,27 @@ const GridRep = (props: Props) => {
)
return (
<div className="GridRep">
{props.displayUser ? detailsWithUsername : details}
<div className="Grid" onClick={navigate}>
<div className="weapon grid_mainhand">{generateMainhandImage()}</div>
<Link href={`/p/${props.shortcode}`}>
<a className="GridRep">
{props.displayUser ? detailsWithUsername : details}
<div className="Grid">
<div className="weapon grid_mainhand">{generateMainhandImage()}</div>
<ul className="grid_weapons">
{Array.from(Array(numWeapons)).map((x, i) => {
return (
<li
key={`${props.shortcode}-${i}`}
className="weapon grid_weapon"
>
{generateGridImage(i)}
</li>
)
})}
</ul>
</div>
</div>
<ul className="grid_weapons">
{Array.from(Array(numWeapons)).map((x, i) => {
return (
<li
key={`${props.shortcode}-${i}`}
className="weapon grid_weapon"
>
{generateGridImage(i)}
</li>
)
})}
</ul>
</div>
</a>
</Link>
)
}

View file

@ -1,6 +1,6 @@
.GridRepCollection {
display: grid;
grid-template-columns: auto auto auto;
grid-template-columns: 1fr 1fr 1fr;
margin: 0 auto;
padding: 0;
width: fit-content;

View file

@ -1,6 +1,6 @@
.Header {
display: flex;
margin-bottom: $unit-2x;
margin-bottom: $unit;
width: 100%;
&.bottom {
@ -16,9 +16,10 @@
.dropdown {
display: inline-block;
position: relative;
padding-bottom: $unit;
&:hover {
padding-right: 50px;
padding-right: $unit-4x;
.Button {
background: var(--button-bg-hover);

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import Select from '~components/Select'
import SelectItem from '~components/SelectItem'
@ -26,6 +27,9 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
const router = useRouter()
const locale = router.locale || 'en'
// Set up translation
const { t } = useTranslation('common')
// Create snapshot of app state
const { party } = useSnapshot(appState)
@ -106,11 +110,12 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
placeholder={'Select a class...'}
open={open}
onClick={openJobSelect}
onOpenChange={() => setOpen(!open)}
onValueChange={handleChange}
triggerClass="Job"
>
<SelectItem key={-1} value="no-job">
No class
{t('no_job')}
</SelectItem>
{sortedJobs
? Object.keys(sortedJobs)

View file

@ -3,29 +3,4 @@
flex-direction: column;
gap: calc($unit / 2);
margin-bottom: $unit;
.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-50;
&:hover {
background: $grey-80;
}
}
}
input {
background: $grey-90;
}
}

View file

@ -171,6 +171,15 @@ const LoginModal = (props: Props) => {
})
}
function onEscapeKeyDown(event: KeyboardEvent) {
setOpen(false)
}
function onOpenAutoFocus(event: Event) {
event.preventDefault()
if (emailInput.current) emailInput.current.focus()
}
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>
@ -178,7 +187,11 @@ const LoginModal = (props: Props) => {
<span>{t('menu.login')}</span>
</li>
</DialogTrigger>
<DialogContent className="Login Dialog">
<DialogContent
className="Login Dialog"
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<div className="DialogHeader">
<div className="DialogTitle">
<h1>{t('modals.login.title')}</h1>
@ -206,7 +219,10 @@ const LoginModal = (props: Props) => {
ref={passwordInput}
/>
<Button text={t('modals.login.buttons.confirm')} />
<Button
disabled={!formValid}
text={t('modals.login.buttons.confirm')}
/>
</form>
</DialogContent>
</Dialog>

View file

@ -238,6 +238,8 @@ const Party = (props: Props) => {
{navigation}
<section id="Party">{currentGrid()}</section>
<PartyDetails
party={props.team}
new={props.new || false}
editable={party.editable}
updateCallback={updateDetails}
deleteCallback={deleteTeam}

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
@ -13,18 +14,20 @@ 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 { formatTimeAgo } from '~utils/timeAgo'
import CheckIcon from '~public/icons/Check.svg'
import CrossIcon from '~public/icons/Cross.svg'
import EditIcon from '~public/icons/Edit.svg'
import './index.scss'
import Link from 'next/link'
import { formatTimeAgo } from '~utils/timeAgo'
// Props
interface Props {
party?: Party
new: boolean
editable: boolean
updateCallback: (name?: string, description?: string, raid?: Raid) => void
deleteCallback: (
@ -42,23 +45,24 @@ const PartyDetails = (props: Props) => {
const nameInput = React.createRef<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>()
const [open, setOpen] = useState(false)
const [raidSlug, setRaidSlug] = useState('')
const readOnlyClasses = classNames({
PartyDetails: true,
ReadOnly: true,
Visible: !party.detailsVisible,
Visible: true,
})
const editableClasses = classNames({
PartyDetails: true,
Editable: true,
Visible: party.detailsVisible,
Visible: open,
})
const emptyClasses = classNames({
EmptyDetails: true,
Visible: !party.detailsVisible,
Visible: true,
})
const userClass = classNames({
@ -99,7 +103,7 @@ const PartyDetails = (props: Props) => {
}
function toggleDetails() {
appState.party.detailsVisible = !appState.party.detailsVisible
setOpen(!open)
}
function receiveRaid(slug?: string) {
@ -115,34 +119,57 @@ const PartyDetails = (props: Props) => {
toggleDetails()
}
const userImage = () => {
if (party.user)
const userImage = (picture?: string, element?: string) => {
if (picture && element)
return (
<img
alt={party.user.avatar.picture}
className={`profile ${party.user.avatar.element}`}
srcSet={`/profile/${party.user.avatar.picture}.png,
/profile/${party.user.avatar.picture}@2x.png 2x`}
src={`/profile/${party.user.avatar.picture}.png`}
alt={picture}
className={`profile ${element}`}
srcSet={`/profile/${picture}.png,
/profile/${picture}@2x.png 2x`}
src={`/profile/${picture}.png`}
/>
)
else return <div className="no-user" />
}
const userBlock = () => {
const userBlock = (username?: string, picture?: string, element?: string) => {
return (
<div className={userClass}>
{userImage()}
{party.user ? party.user.username : t('no_user')}
{userImage(picture, element)}
{username ? username : t('no_user')}
</div>
)
}
const linkedUserBlock = (user: User) => {
const renderUserBlock = () => {
let username, picture, element
if (accountState.account.authorized && props.new) {
username = accountState.account.user?.username
picture = accountState.account.user?.picture
element = accountState.account.user?.element
} else if (party.user && !props.new) {
username = party.user.username
picture = party.user.avatar.picture
element = party.user.avatar.element
}
if (username && picture && element) {
return linkedUserBlock(username, picture, element)
} else if (!props.new) {
return userBlock()
}
}
const linkedUserBlock = (
username?: string,
picture?: string,
element?: string
) => {
return (
<div>
<Link href={`/${user.username}`} passHref>
<a className={linkClass}>{userBlock()}</a>
<Link href={`/${username}`} passHref>
<a className={linkClass}>{userBlock(username, picture, element)}</a>
</Link>
</div>
)
@ -202,7 +229,7 @@ const PartyDetails = (props: Props) => {
<CharLimitedFieldset
fieldName="name"
placeholder="Name your team"
value={party.name}
value={props.party?.name}
limit={50}
onChange={handleInputChange}
error={errors.name}
@ -210,7 +237,7 @@ const PartyDetails = (props: Props) => {
/>
<RaidDropdown
showAllRaidsOption={false}
currentRaid={party.raid ? party.raid.slug : undefined}
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
onChange={receiveRaid}
/>
<TextFieldset
@ -218,7 +245,7 @@ const PartyDetails = (props: Props) => {
placeholder={
'Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!'
}
value={party.description}
value={props.party?.description}
onChange={handleTextAreaChange}
error={errors.description}
ref={descriptionInput}
@ -248,9 +275,9 @@ const PartyDetails = (props: Props) => {
{party.name ? party.name : 'Untitled'}
</h1>
<div className="attribution">
{party.user ? linkedUserBlock(party.user) : userBlock()}
{renderUserBlock()}
{party.raid ? linkedRaidBlock(party.raid) : ''}
{party.created_at != undefined ? (
{party.created_at != '' ? (
<time
className="last-updated"
dateTime={new Date(party.created_at).toString()}
@ -284,25 +311,9 @@ const PartyDetails = (props: Props) => {
</section>
)
const emptyDetails = (
<div className={emptyClasses}>
{party.editable ? (
<Button
accessoryIcon={<EditIcon />}
text={t('buttons.show_info')}
onClick={toggleDetails}
/>
) : (
<div />
)}
</div>
)
return (
<React.Fragment>
{editable && (party.name || party.description || party.raid)
? readOnly
: emptyDetails}
{readOnly}
{editable}
</React.Fragment>
)

View file

@ -123,6 +123,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
value={props.currentRaid}
placeholder={'Select a raid...'}
open={open}
onOpenChange={() => setOpen(!open)}
onClick={openRaidSelect}
onValueChange={handleChange}
>

View file

@ -340,10 +340,24 @@ const SearchModal = (props: Props) => {
}
}
function onEscapeKeyDown(event: KeyboardEvent) {
event.preventDefault()
openChange()
}
function onOpenAutoFocus(event: Event) {
event.preventDefault()
if (searchInput.current) searchInput.current.focus()
}
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent className="Search Dialog">
<DialogContent
className="Search Dialog"
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<div id="Header">
<div id="Bar">
<Input

View file

@ -15,8 +15,9 @@ interface Props
open: boolean
trigger?: React.ReactNode
children?: React.ReactNode
onClick?: () => void
onOpenChange?: () => void
onValueChange?: (value: string) => void
onClose?: () => void
triggerClass?: string
}
@ -24,8 +25,13 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
props: Props,
forwardedRef
) {
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
useEffect(() => {
setOpen(props.open)
}, [props.open])
useEffect(() => {
if (props.value && props.value !== '') setValue(`${props.value}`)
else setValue('')
@ -36,10 +42,27 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
if (props.onValueChange) props.onValueChange(newValue)
}
function onCloseAutoFocus() {
setOpen(false)
if (props.onClose) props.onClose()
}
function onEscapeKeyDown() {
setOpen(false)
if (props.onClose) props.onClose()
}
function onPointerDownOutside() {
setOpen(false)
if (props.onClose) props.onClose()
}
return (
<RadixSelect.Root
open={open}
value={value !== '' ? value : undefined}
onValueChange={onValueChange}
onOpenChange={props.onOpenChange}
>
<RadixSelect.Trigger
className={classNames('SelectTrigger', props.triggerClass)}
@ -53,7 +76,11 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
</RadixSelect.Trigger>
<RadixSelect.Portal className="Select">
<RadixSelect.Content>
<RadixSelect.Content
onCloseAutoFocus={onCloseAutoFocus}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
>
<RadixSelect.ScrollUpButton className="Scroll Up">
<ArrowIcon />
</RadixSelect.ScrollUpButton>

View file

@ -15,8 +15,9 @@ interface Props {
imageClass?: string
imageSrc?: string[]
children: React.ReactNode
onClick: () => void
onOpenChange: () => void
onChange: (value: string) => void
onClose: () => void
}
const SelectTableField = (props: Props) => {
@ -53,8 +54,9 @@ const SelectTableField = (props: Props) => {
<Select
name={props.name}
open={props.open}
onClick={props.onClick}
onOpenChange={props.onOpenChange}
onValueChange={props.onChange}
onClose={props.onClose}
triggerClass={classNames({ Bound: true, Table: true })}
value={value}
>

View file

@ -4,27 +4,6 @@
gap: calc($unit / 2);
margin-bottom: $unit;
.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-50;
&:hover {
background: $grey-80;
}
}
}
.terms {
color: $grey-50;
font-size: $font-small;
@ -40,8 +19,4 @@
}
}
}
input {
background: $grey-90;
}
}

View file

@ -263,6 +263,15 @@ const SignupModal = (props: Props) => {
})
}
function onEscapeKeyDown(event: KeyboardEvent) {
setOpen(false)
}
function onOpenAutoFocus(event: Event) {
event.preventDefault()
if (usernameInput.current) usernameInput.current.focus()
}
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>
@ -270,7 +279,11 @@ const SignupModal = (props: Props) => {
<span>{t('menu.signup')}</span>
</li>
</DialogTrigger>
<DialogContent className="Signup Dialog">
<DialogContent
className="Signup Dialog"
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<div className="DialogHeader">
<div className="DialogTitle">
<h1>{t('modals.signup.title')}</h1>
@ -315,7 +328,10 @@ const SignupModal = (props: Props) => {
ref={passwordConfirmationInput}
/>
<Button text={t('modals.signup.buttons.confirm')} />
<Button
disabled={!formValid}
text={t('modals.signup.buttons.confirm')}
/>
<p className="terms">
{/* <Trans i18nKey="modals.signup.agreement">

View file

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect } from 'react'
import UncapStar from '~components/UncapStar'
import './index.scss'
@ -10,12 +10,10 @@ interface Props {
flb: boolean
ulb: boolean
special: boolean
updateUncap?: (uncap: number) => void
updateUncap?: (index: number) => void
}
const UncapIndicator = (props: Props) => {
const [uncap, setUncap] = useState(props.uncapLevel)
const numStars = setNumStars()
function setNumStars() {
let numStars
@ -53,7 +51,7 @@ const UncapIndicator = (props: Props) => {
function toggleStar(index: number, empty: boolean) {
if (props.updateUncap) {
if (empty) props.updateUncap(index + 1)
if (empty && index > 0) props.updateUncap(index + 1)
else props.updateUncap(index)
}
}
@ -71,10 +69,11 @@ const UncapIndicator = (props: Props) => {
}
const ulb = (i: number) => {
// console.log('ULB; Number of stars:', props.uncapLevel)
return (
<UncapStar
ulb={true}
empty={props.uncapLevel ? i >= props.uncapLevel : false}
empty={props.uncapLevel != null ? i >= props.uncapLevel : false}
key={`star_${i}`}
index={i}
onClick={toggleStar}
@ -83,10 +82,11 @@ const UncapIndicator = (props: Props) => {
}
const flb = (i: number) => {
// console.log('FLB; Number of stars:', props.uncapLevel)
return (
<UncapStar
flb={true}
empty={props.uncapLevel ? i >= props.uncapLevel : false}
empty={props.uncapLevel != null ? i >= props.uncapLevel : false}
key={`star_${i}`}
index={i}
onClick={toggleStar}
@ -95,10 +95,10 @@ const UncapIndicator = (props: Props) => {
}
const mlb = (i: number) => {
// console.log("MLB; Number of stars:", props.uncapLevel)
// console.log('MLB; Number of stars:', props.uncapLevel)
return (
<UncapStar
empty={props.uncapLevel ? i >= props.uncapLevel : false}
empty={props.uncapLevel != null ? i >= props.uncapLevel : false}
key={`star_${i}`}
index={i}
onClick={toggleStar}

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import classnames from 'classnames'
import './index.scss'

View file

@ -96,7 +96,7 @@ const WeaponGrid = (props: Props) => {
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveWeapon(party.id, weapon, position).then((response) =>
storeGridWeapon(response.data)
storeGridWeapon(response.data.grid_weapon)
)
})
} else {
@ -252,6 +252,7 @@ const WeaponGrid = (props: Props) => {
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
console.log(`Updating uncap level at position ${position} to ${uncapLevel}`)
if (appState.grid.weapons.mainWeapon && position == -1)
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel
else {

View file

@ -9,10 +9,13 @@ import './index.scss'
// Props
interface Props {
open: boolean
currentValue?: WeaponKey
series: number
slot: number
onChange?: (value: string, slot: number) => void
onOpenChange: () => void
onClose?: () => void
}
const WeaponKeySelect = React.forwardRef<HTMLButtonElement, Props>(
@ -64,10 +67,6 @@ const WeaponKeySelect = React.forwardRef<HTMLButtonElement, Props>(
fetchWeaponKeys()
}, [props.series, props.slot])
function openSelect() {
setOpen(!open)
}
function weaponKeyGroup(index: number) {
;['α', 'β', 'γ', 'Δ'].sort((a, b) => a.localeCompare(b, 'el'))
@ -125,9 +124,10 @@ const WeaponKeySelect = React.forwardRef<HTMLButtonElement, Props>(
<Select
key={`weapon-key-${props.slot}`}
value={props.currentValue ? props.currentValue.id : 'no-key'}
open={open}
open={props.open}
onClose={props.onClose}
onOpenChange={props.onOpenChange}
onValueChange={handleChange}
onClick={openSelect}
ref={ref}
triggerClass="modal"
>

View file

@ -4,7 +4,13 @@ import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog'
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
DialogTrigger,
} from '~components/Dialog'
import AXSelect from '~components/AxSelect'
import AwakeningSelect from '~components/AwakeningSelect'
@ -74,6 +80,14 @@ const WeaponModal = (props: Props) => {
const [weaponKey2Id, setWeaponKey2Id] = useState('')
const [weaponKey3Id, setWeaponKey3Id] = useState('')
const [weaponKey1Open, setWeaponKey1Open] = useState(false)
const [weaponKey2Open, setWeaponKey2Open] = useState(false)
const [weaponKey3Open, setWeaponKey3Open] = useState(false)
const [weaponKey4Open, setWeaponKey4Open] = useState(false)
const [ax1Open, setAx1Open] = useState(false)
const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false)
useEffect(() => {
setElement(props.gridWeapon.element)
@ -188,16 +202,35 @@ const WeaponModal = (props: Props) => {
)
}
function openSelect(index: 1 | 2 | 3 | 4) {
setWeaponKey1Open(index === 1 ? !weaponKey1Open : false)
setWeaponKey2Open(index === 2 ? !weaponKey2Open : false)
setWeaponKey3Open(index === 3 ? !weaponKey3Open : false)
setWeaponKey4Open(index === 4 ? !weaponKey4Open : false)
}
function receiveAxOpen(index: 1 | 2, isOpen: boolean) {
if (index === 1) setAx1Open(isOpen)
if (index === 2) setAx2Open(isOpen)
}
function receiveAwakeningOpen(isOpen: boolean) {
setAwakeningOpen(isOpen)
}
const keySelect = () => {
return (
<section>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey1Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined}
series={props.gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(1)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey1Open(false)}
/>
) : (
''
@ -205,10 +238,13 @@ const WeaponModal = (props: Props) => {
{[2, 3, 17].includes(props.gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey2Open}
currentValue={weaponKey2 != null ? weaponKey2 : undefined}
series={props.gridWeapon.object.series}
slot={1}
onOpenChange={() => openSelect(2)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey2Open(false)}
/>
) : (
''
@ -216,10 +252,13 @@ const WeaponModal = (props: Props) => {
{props.gridWeapon.object.series == 17 ? (
<WeaponKeySelect
open={weaponKey3Open}
currentValue={weaponKey3 != null ? weaponKey3 : undefined}
series={props.gridWeapon.object.series}
slot={2}
onOpenChange={() => openSelect(3)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey3Open(false)}
/>
) : (
''
@ -228,10 +267,13 @@ const WeaponModal = (props: Props) => {
{props.gridWeapon.object.series == 24 &&
props.gridWeapon.object.uncap.ulb ? (
<WeaponKeySelect
open={weaponKey4Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined}
series={props.gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(4)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey4Open(false)}
/>
) : (
''
@ -247,6 +289,7 @@ const WeaponModal = (props: Props) => {
<AXSelect
axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax}
onOpenChange={receiveAxOpen}
sendValidity={receiveValidity}
sendValues={receiveAxValues}
/>
@ -262,6 +305,7 @@ const WeaponModal = (props: Props) => {
object="weapon"
awakeningType={props.gridWeapon.awakening?.type}
awakeningLevel={props.gridWeapon.awakening?.level}
onOpenChange={receiveAwakeningOpen}
sendValidity={receiveValidity}
sendValues={receiveAwakeningValues}
/>
@ -278,49 +322,64 @@ const WeaponModal = (props: Props) => {
setOpen(open)
}
const anySelectOpen =
weaponKey1Open ||
weaponKey2Open ||
weaponKey3Open ||
weaponKey4Open ||
ax1Open ||
ax2Open ||
awakeningOpen
function onEscapeKeyDown(event: KeyboardEvent) {
if (anySelectOpen) {
return event.preventDefault()
} else {
setOpen(false)
}
}
return (
// TODO: Refactor into Dialog component
<Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content
className="Weapon Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader">
<div className="DialogTop">
<Dialog.Title className="SubTitle">
{t('modals.weapon.title')}
</Dialog.Title>
<Dialog.Title className="DialogTitle">
{props.gridWeapon.object.name[locale]}
</Dialog.Title>
</div>
<Dialog.Close className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</Dialog.Close>
<Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent
className="Weapon Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader">
<div className="DialogTop">
<DialogTitle className="SubTitle">
{t('modals.weapon.title')}
</DialogTitle>
<DialogTitle className="DialogTitle">
{props.gridWeapon.object.name[locale]}
</DialogTitle>
</div>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series)
? keySelect()
: ''}
{props.gridWeapon.object.ax > 0 ? axSelect() : ''}
{props.gridWeapon.awakening ? awakeningSelect() : ''}
<Button
contained={true}
onClick={updateWeapon}
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>
</div>
</Dialog.Content>
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
<div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series)
? keySelect()
: ''}
{props.gridWeapon.object.ax > 0 ? axSelect() : ''}
{props.gridWeapon.awakening ? awakeningSelect() : ''}
<Button
contained={true}
onClick={updateWeapon}
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -25,33 +25,6 @@ const Proficiency = [
'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()

View file

@ -15,12 +15,8 @@ import {
emptyRarityState,
emptyWeaponSeriesState,
} from '~utils/emptyStates'
import {
elements,
proficiencies,
rarities,
weaponSeries,
} from '~utils/stateValues'
import { elements, proficiencies, rarities } from '~utils/stateValues'
import { weaponSeries } from '~utils/weaponSeries'
interface Props {
sendFilters: (filters: { [key: string]: number[] }) => void
@ -128,6 +124,45 @@ const WeaponSearchFilterBar = (props: Props) => {
props.sendFilters(filters)
}
const renderWeaponSeries = () => {
const numColumns = 3
return (
<React.Fragment>
{Array.from({ length: numColumns }, () => 0).map((x, i) => {
return renderWeaponSeriesGroup(i)
})}
</React.Fragment>
)
}
const renderWeaponSeriesGroup = (index: number) => {
return (
<DropdownMenu.Group className="Group">
{weaponSeries
.slice(
index * Math.ceil(weaponSeries.length / 3),
(index + 1) * Math.ceil(weaponSeries.length / 3)
)
.map((x, i) => {
return renderSingleWeaponSeries(x.id, x.slug)
})}
</DropdownMenu.Group>
)
}
const renderSingleWeaponSeries = (id: number, slug: string) => {
return (
<SearchFilterCheckboxItem
key={id}
onCheckedChange={handleSeriesChange}
checked={seriesState[slug].checked}
valueKey={slug}
>
{t(`series.${slug}`)}
</SearchFilterCheckboxItem>
)
}
useEffect(() => {
sendFilters()
}, [rarityState, elementState, proficiencyState, seriesState])
@ -254,58 +289,7 @@ const WeaponSearchFilterBar = (props: Props) => {
<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>
<section>{renderWeaponSeries()}</section>
</SearchFilter>
</div>
)

View file

@ -11,7 +11,6 @@ import Button from '~components/Button'
import type { SearchableObject } from '~types'
import { appState } from '~utils/appState'
import { axData } from '~utils/axData'
import { weaponAwakening } from '~utils/awakening'
@ -341,9 +340,9 @@ const WeaponUnit = (props: Props) => {
} else return
}
function passUncapData(uncap: number) {
function passUncapData(index: number) {
if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, props.position, uncap)
props.updateUncap(props.gridWeapon.id, props.position, index)
}
function canBeModified(gridWeapon: GridWeapon) {

View file

@ -246,15 +246,27 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
return (
<div id="Profile">
<Head>
<title>@{props.user?.username}&apos;s Teams</title>
{/* HTML */}
<title>
{t('page.titles.profile', { username: props.user?.username })}
</title>
<meta
name="description"
content={t('page.descriptions.profile', {
username: props.user?.username,
})}
/>
{/* OpenGraph */}
<meta
property="og:title"
content={`@${props.user?.username}\'s Teams`}
content={t('page.titles.profile', { username: props.user?.username })}
/>
<meta
property="og:description"
content={`Browse @${props.user?.username}\'s Teams and filter raid, element or recency`}
content={t('page.descriptions.profile', {
username: props.user?.username,
})}
/>
<meta
property="og:url"
@ -262,15 +274,18 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
/>
<meta property="og:type" content="website" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" />
<meta
name="twitter:title"
content={`@${props.user?.username}\'s Teams`}
content={t('page.titles.profile', { username: props.user?.username })}
/>
<meta
name="twitter:description"
content={`Browse @${props.user?.username}\''s Teams and filter raid, element or recency`}
content={t('page.descriptions.profile', {
username: props.user?.username,
})}
/>
</Head>
<FilterBar

View file

@ -1,5 +1,6 @@
import React, { useEffect } from 'react'
import { getCookie } from 'cookies-next'
import Head from 'next/head'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from '~components/Party'
@ -12,6 +13,9 @@ import api from '~utils/api'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { GroupedWeaponKeys } from '~utils/groupWeaponKeys'
import { useSnapshot } from 'valtio'
import { raidGroups } from '~utils/raidGroups'
import { useRouter } from 'next/router'
interface Props {
party: Party
@ -20,9 +24,18 @@ interface Props {
raids: Raid[]
sortedRaids: Raid[][]
weaponKeys: GroupedWeaponKeys
meta: { [key: string]: string }
}
const PartyRoute: React.FC<Props> = (props: Props) => {
// Import translations
const { t } = useTranslation('common')
// Set up router
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
useEffect(() => {
persistStaticData()
}, [persistStaticData])
@ -34,7 +47,65 @@ const PartyRoute: React.FC<Props> = (props: Props) => {
appState.weaponKeys = props.weaponKeys
}
return <Party team={props.party} raids={props.sortedRaids} />
function generateTitle() {
const teamName =
props.party && props.party.name ? props.party.name : t('no_title')
const username = props.party
? `@${props.party.user.username}`
: t('no_user')
const title = t('page.titles.team', {
username: username,
teamName: teamName,
emoji: props.meta.element,
})
return title
}
return (
<React.Fragment>
<Party team={props.party} raids={props.sortedRaids} />
<Head>
{/* HTML */}
<title>{generateTitle()}</title>
<meta
name="description"
content={t('page.descriptions.team', {
username: props.party.user?.username,
raidName: props.party.raid.name[locale],
})}
/>
{/* OpenGraph */}
<meta property="og:title" content={generateTitle()} />
<meta
property="og:description"
content={t('page.descriptions.team', {
username: props.party.user?.username,
raidName: props.party.raid.name[locale],
})}
/>
<meta
property="og:url"
content={`https://app.granblue.team/p/${props.party.shortcode}`}
/>
<meta property="og:type" content="website" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content={generateTitle()} />
<meta
name="twitter:description"
content={t('page.descriptions.team', {
username: props.party.user?.username,
raidName: props.party.raid.name[locale],
})}
/>
</Head>
</React.Fragment>
)
}
export const getServerSidePaths = async () => {
@ -80,6 +151,31 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
console.log('No party code')
}
function getElement() {
if (party) {
const mainhand = party.weapons.find((weapon) => weapon.mainhand)
if (mainhand && mainhand.object.element === 0) {
return mainhand.element
} else {
return mainhand?.object.element
}
} else {
return 0
}
}
function elementEmoji() {
const element = getElement()
if (element === 0) return '⚪'
if (element === 1) return '🟢'
if (element === 2) return '🔴'
if (element === 3) return '🔵'
if (element === 4) return '🟤'
if (element === 5) return '🟣'
if (element === 6) return '🟡'
}
return {
props: {
party: party,
@ -88,6 +184,9 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
raids: raids,
sortedRaids: sortedRaids,
weaponKeys: weaponKeys,
meta: {
element: elementEmoji()
},
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},

View file

@ -288,15 +288,15 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
return (
<div id="Teams">
<Head>
<title>{t('saved.title')}</title>
<title>{t('page.titles.saved')}</title>
<meta property="og:title" content="Your saved Teams" />
<meta property="og:title" content={t('page.titles.saved')} />
<meta property="og:url" content="https://app.granblue.team/saved" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content="Your saved Teams" />
<meta name="twitter:title" content={t('page.titles.saved')} />
</Head>
<FilterBar

View file

@ -287,22 +287,26 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
return (
<div id="Teams">
<Head>
<title>{t('teams.title')}</title>
{/* HTML */}
<title>{t('page.titles.discover')}</title>
<meta name="description" content={t('page.descriptions.discover')} />
<meta property="og:title" content="Discover Teams" />
{/* OpenGraph */}
<meta property="og:title" content={t('page.titles.discover')} />
<meta
property="og:description"
content="Find different Granblue Fantasy teams by raid, element or recency"
content={t('page.descriptions.discover')}
/>
<meta property="og:url" content="https://app.granblue.team/teams" />
<meta property="og:type" content="website" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content="Discover Teams" />
<meta name="twitter:title" content={t('page.titles.discover')} />
<meta
name="twitter:description"
content="Find different Granblue Fantasy teams by raid, element or recency"
content={t('page.descriptions.discover')}
/>
</Head>

View file

@ -100,7 +100,7 @@
"bahamut": "Bahamut",
"epic": "Epic",
"ennead": "Ennead",
"cosmos": "Cosmos",
"cosmic": "Cosmic",
"ancestral": "Ancestral",
"superlative": "Superlative",
"vintage": "Vintage",
@ -109,7 +109,8 @@
"new_world": "New World Foundation",
"revenant": "Revenant",
"proving": "Proving Grounds",
"disaster": "Disaster"
"disaster": "Disaster",
"illustrious": "Illustrious"
},
"recency": {
"all_time": "All time",
@ -277,6 +278,19 @@
"loading": "Loading teams...",
"not_found": "No teams found"
},
"page": {
"titles": {
"discover": "Discover teams",
"profile": "@{{username}}'s Teams / granblue.team",
"team": "{{emoji}} {{teamName}} by {{username}} / granblue.team",
"saved": "Your saved teams"
},
"descriptions": {
"discover": "Save and discover teams to use in Granblue Fantasy and search by raid, element or recency",
"profile": "Browse @{{username}}'s Teams and filter by raid, element or recency",
"team": "Browse this team for {{raidName}} by {{username}} and others on granblue.team"
}
},
"job_skills": {
"all": "All skills",
"buffing": "Buffing",
@ -294,5 +308,6 @@
"coming_soon": "Coming Soon",
"no_title": "Untitled",
"no_raid": "No raid",
"no_user": "Anonymous"
"no_user": "Anonymous",
"no_job": "No class"
}

View file

@ -100,7 +100,7 @@
"bahamut": "バハムートウェポン",
"epic": "エピックウェポン",
"ennead": "エニアドシリーズ",
"cosmos": "コスモスシリーズ",
"cosmic": "コスモスシリーズ",
"ancestral": "アンセスタルシリーズ",
"superlative": "スペリオシリーズ",
"vintage": "ヴィンテージシリーズ",
@ -109,7 +109,8 @@
"new_world": "新世界の礎",
"revenant": "天星器",
"proving": "ブレイブグラウンド",
"disaster": "災害シリーズ"
"disaster": "災害シリーズ",
"luminous": "ルミナス"
},
"recency": {
"all_time": "全ての期間",
@ -278,6 +279,19 @@
"loading": "ロード中...",
"not_found": "編成は見つかりませんでした"
},
"page": {
"titles": {
"discover": "編成を見出す",
"profile": "@{{username}}さんの作った編成",
"team": "{{emoji}} {{teamName}}、{{username}}さんから / granblue.team",
"saved": "保存した編成"
},
"descriptions": {
"discover": "グランブルーファンタジーの編成をマルチ、属性、作った時間などで探したり保存したりできる",
"profile": "@{{username}}の編成を調査し、マルチ、属性、または作った時間でフィルターする",
"team": "granblue.teamで{{username}}さんが作った{{raidName}}の編成を調査できる"
}
},
"job_skills": {
"all": "全てのアビリティ",
"buffing": "強化アビリティ",
@ -291,9 +305,10 @@
"no_skill": "設定されていません"
}
},
"extra_weapons": "Additional<br/>Weapons",
"extra_weapons": "Additional Weapons",
"coming_soon": "開発中",
"no_title": "無題",
"no_raid": "マルチなし",
"no_user": "無名"
"no_user": "無名",
"no_job": "ジョブなし"
}

View file

@ -33,27 +33,27 @@ a {
text-decoration: none;
&.wind {
color: $wind-text-10;
color: var(--wind-bg);
}
&.fire {
color: $fire-text-10;
color: var(--fire-bg);
}
&.water {
color: $water-text-10;
color: var(--water-bg);
}
&.earth {
color: $earth-text-10;
color: var(--earth-bg);
}
&.dark {
color: $dark-text-10;
color: var(--dark-bg);
}
&.light {
color: $light-text-10;
color: var(--light-bg);
}
}
@ -69,7 +69,7 @@ h1,
h2,
h3,
p {
color: $grey-15;
color: var(--text-primary);
}
h1 {

View file

@ -7,6 +7,8 @@
--card-bg: #{$grey-100};
--bar-bg: #{$grey-100};
--link-text-hover: #{$text--link--hover--light};
// Light - Menus
--dialog-bg: #{$dialog--bg--light};
@ -120,6 +122,8 @@
--card-bg: #{$page--element--bg--dark};
--bar-bg: #{$grey-10};
--link-text-hover: #{$text--link--hover--dark};
// Dark - Dialogs
--dialog-bg: #{$dialog--bg--dark};

View file

@ -55,6 +55,7 @@ $orange-10: #6b401b;
$orange-30: #825b39;
$orange-40: #925a2a;
$orange-50: #a8703f;
$orange-70: #d08f57;
$orange-80: #facea7;
$orange-90: #ffebd9;
@ -213,7 +214,7 @@ $subaura--orange--bg--dark: $orange-10;
$subaura--orange--card--bg--dark: $orange-30;
$subaura--orange--primary--dark: $orange-00;
$subaura--orange--secondary--dark: $orange-10;
$subaura--orange--text--dark: $orange-00;
$subaura--orange--text--dark: $orange-70;
// Color Definitions: Element Toggle
$toggle--bg--light: $grey-90;
@ -235,6 +236,9 @@ $text--tertiary--color--dark: $grey-50;
$text--tertiary--hover--light: $grey-40;
$text--tertiary--hover--dark: $grey-70;
$text--link--hover--light: $grey-40;
$text--link--hover--dark: $grey-100;
// Color Definitions: Icon
$icon--secondary--color--light: $grey-70;
$icon--secondary--color--dark: $grey-50;

View file

@ -17,7 +17,7 @@ interface WeaponSeriesState {
astral: CheckedState
epic: CheckedState
ennead: CheckedState
cosmos: CheckedState
cosmic: CheckedState
ancestral: CheckedState
superlative: CheckedState
vintage: CheckedState

View file

@ -80,8 +80,8 @@ export const initialAppState: AppState = {
extra: false,
user: undefined,
favorited: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
created_at: '',
updated_at: '',
},
grid: {
weapons: {

View file

@ -100,12 +100,16 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 3,
checked: false,
},
ultima: {
id: 17,
revenant: {
id: 4,
checked: false,
},
bahamut: {
id: 16,
primal: {
id: 6,
checked: false,
},
beast: {
id: 7,
checked: false,
},
regalia: {
@ -116,10 +120,6 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 9,
checked: false,
},
primal: {
id: 6,
checked: false,
},
olden_primal: {
id: 10,
checked: false,
@ -128,26 +128,30 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 11,
checked: false,
},
beast: {
id: 7,
checked: false,
},
rose: {
id: 15,
hollowsky: {
id: 12,
checked: false,
},
xeno: {
id: 13,
checked: false,
},
hollowsky: {
id: 12,
checked: false,
},
astral: {
id: 14,
checked: false,
},
rose: {
id: 15,
checked: false,
},
bahamut: {
id: 16,
checked: false,
},
ultima: {
id: 17,
checked: false,
},
epic: {
id: 18,
checked: false,
@ -156,7 +160,7 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 19,
checked: false,
},
cosmos: {
cosmic: {
id: 20,
checked: false,
},
@ -176,6 +180,10 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 24,
checked: false,
},
proving: {
id: 25,
checked: false,
},
sephira: {
id: 28,
checked: false,
@ -184,6 +192,14 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
id: 29,
checked: false,
},
disaster: {
id: 30,
checked: false,
},
illustrious: {
id: 31,
checked: false,
},
}
export const emptyPaginationObject = {

View file

@ -116,4 +116,8 @@ export const weaponSeries: WeaponSeries[] = [
id: 30,
slug: 'disaster',
},
{
id: 31,
slug: 'illustrious',
},
]