Merge branch 'staging' of github.com:jedmund/hensei-web into staging
This commit is contained in:
commit
d8f70ff8a0
54 changed files with 12188 additions and 3327 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -57,6 +57,7 @@ public/images/accessory*
|
||||||
public/images/mastery*
|
public/images/mastery*
|
||||||
public/images/updates*
|
public/images/updates*
|
||||||
public/images/guidebooks*
|
public/images/guidebooks*
|
||||||
|
public/images/raids*
|
||||||
|
|
||||||
# Typescript v1 declaration files
|
# Typescript v1 declaration files
|
||||||
typings/
|
typings/
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
select,
|
select,
|
||||||
.SelectTrigger {
|
.SelectTrigger {
|
||||||
// background: url("/icons/Arrow.svg"), $grey-90;
|
// background: url("/icons/Chevron.svg"), $grey-90;
|
||||||
// background-repeat: no-repeat;
|
// background-repeat: no-repeat;
|
||||||
// background-position-y: center;
|
// background-position-y: center;
|
||||||
// background-position-x: 95%;
|
// background-position-x: 95%;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import classNames from 'classnames'
|
||||||
import equals from 'fast-deep-equal'
|
import equals from 'fast-deep-equal'
|
||||||
|
|
||||||
import FilterModal from '~components/FilterModal'
|
import FilterModal from '~components/FilterModal'
|
||||||
import RaidDropdown from '~components/RaidDropdown'
|
|
||||||
import Select from '~components/common/Select'
|
import Select from '~components/common/Select'
|
||||||
import SelectItem from '~components/common/SelectItem'
|
import SelectItem from '~components/common/SelectItem'
|
||||||
import Button from '~components/common/Button'
|
import Button from '~components/common/Button'
|
||||||
|
|
@ -15,6 +14,8 @@ import FilterIcon from '~public/icons/Filter.svg'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { getCookie } from 'cookies-next'
|
import { getCookie } from 'cookies-next'
|
||||||
|
import RaidCombobox from '~components/raids/RaidCombobox'
|
||||||
|
import { appState } from '~utils/appState'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|
@ -29,6 +30,8 @@ const FilterBar = (props: Props) => {
|
||||||
// Set up translation
|
// Set up translation
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
const [currentRaid, setCurrentRaid] = useState<Raid>()
|
||||||
|
|
||||||
const [recencyOpen, setRecencyOpen] = useState(false)
|
const [recencyOpen, setRecencyOpen] = useState(false)
|
||||||
const [elementOpen, setElementOpen] = useState(false)
|
const [elementOpen, setElementOpen] = useState(false)
|
||||||
|
|
||||||
|
|
@ -47,6 +50,16 @@ const FilterBar = (props: Props) => {
|
||||||
FiltersActive: !matchesDefaultFilters,
|
FiltersActive: !matchesDefaultFilters,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Convert raid slug to Raid object on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const raid = appState.raidGroups
|
||||||
|
.filter((group) => group.section > 0)
|
||||||
|
.flatMap((group) => group.raids)
|
||||||
|
.find((raid) => raid.slug === props.raidSlug)
|
||||||
|
|
||||||
|
setCurrentRaid(raid)
|
||||||
|
}, [props.raidSlug])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch user's advanced filters
|
// Fetch user's advanced filters
|
||||||
const filtersCookie = getCookie('filters')
|
const filtersCookie = getCookie('filters')
|
||||||
|
|
@ -76,8 +89,8 @@ const FilterBar = (props: Props) => {
|
||||||
props.onFilter({ recency: recencyValue, ...advancedFilters })
|
props.onFilter({ recency: recencyValue, ...advancedFilters })
|
||||||
}
|
}
|
||||||
|
|
||||||
function raidSelectChanged(slug?: string) {
|
function raidSelectChanged(raid?: Raid) {
|
||||||
props.onFilter({ raidSlug: slug, ...advancedFilters })
|
props.onFilter({ raidSlug: raid?.slug, ...advancedFilters })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAdvancedFiltersChanged(filters: FilterSet) {
|
function handleAdvancedFiltersChanged(filters: FilterSet) {
|
||||||
|
|
@ -90,6 +103,25 @@ const FilterBar = (props: Props) => {
|
||||||
setRecencyOpen(name === 'recency' ? !recencyOpen : false)
|
setRecencyOpen(name === 'recency' ? !recencyOpen : false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateSelectItems() {
|
||||||
|
const elements = [
|
||||||
|
{ element: 'all', key: -1, value: -1, text: t('elements.full.all') },
|
||||||
|
{ element: 'null', key: 0, value: 0, text: t('elements.full.null') },
|
||||||
|
{ element: 'wind', key: 1, value: 1, text: t('elements.full.wind') },
|
||||||
|
{ element: 'fire', key: 2, value: 2, text: t('elements.full.fire') },
|
||||||
|
{ element: 'water', key: 3, value: 3, text: t('elements.full.water') },
|
||||||
|
{ element: 'earth', key: 4, value: 4, text: t('elements.full.earth') },
|
||||||
|
{ element: 'dark', key: 5, value: 5, text: t('elements.full.dark') },
|
||||||
|
{ element: 'light', key: 6, value: 6, text: t('elements.full.light') },
|
||||||
|
]
|
||||||
|
|
||||||
|
return elements.map(({ element, key, value, text }) => (
|
||||||
|
<SelectItem data-element={element} key={key} value={value}>
|
||||||
|
{text}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
|
|
@ -97,47 +129,26 @@ const FilterBar = (props: Props) => {
|
||||||
<div className="Filters">
|
<div className="Filters">
|
||||||
<Select
|
<Select
|
||||||
value={`${props.element}`}
|
value={`${props.element}`}
|
||||||
|
overlayVisible={false}
|
||||||
open={elementOpen}
|
open={elementOpen}
|
||||||
onOpenChange={() => onSelectChange('element')}
|
onOpenChange={() => onSelectChange('element')}
|
||||||
onValueChange={elementSelectChanged}
|
onValueChange={elementSelectChanged}
|
||||||
onClick={openElementSelect}
|
onClick={openElementSelect}
|
||||||
>
|
>
|
||||||
<SelectItem data-element="all" key={-1} value={-1}>
|
{generateSelectItems()}
|
||||||
{t('elements.full.all')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="null" key={0} value={0}>
|
|
||||||
{t('elements.full.null')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="wind" key={1} value={1}>
|
|
||||||
{t('elements.full.wind')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="fire" key={2} value={2}>
|
|
||||||
{t('elements.full.fire')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="water" key={3} value={3}>
|
|
||||||
{t('elements.full.water')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="earth" key={4} value={4}>
|
|
||||||
{t('elements.full.earth')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="dark" key={5} value={5}>
|
|
||||||
{t('elements.full.dark')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem data-element="light" key={6} value={6}>
|
|
||||||
{t('elements.full.light')}
|
|
||||||
</SelectItem>
|
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<RaidDropdown
|
<RaidCombobox
|
||||||
currentRaid={props.raidSlug}
|
currentRaid={currentRaid}
|
||||||
defaultRaid="all"
|
|
||||||
showAllRaidsOption={true}
|
showAllRaidsOption={true}
|
||||||
|
minimal={true}
|
||||||
onChange={raidSelectChanged}
|
onChange={raidSelectChanged}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={`${props.recency}`}
|
value={`${props.recency}`}
|
||||||
trigger={'All time'}
|
trigger={'All time'}
|
||||||
|
overlayVisible={false}
|
||||||
open={recencyOpen}
|
open={recencyOpen}
|
||||||
onOpenChange={() => onSelectChange('recency')}
|
onOpenChange={() => onSelectChange('recency')}
|
||||||
onValueChange={recencySelectChanged}
|
onValueChange={recencySelectChanged}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
padding-top: $unit-fourth;
|
||||||
transition: opacity 0.14s ease-in-out;
|
transition: opacity 0.14s ease-in-out;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import Button from '~components/common/Button'
|
||||||
import Tooltip from '~components/common/Tooltip'
|
import Tooltip from '~components/common/Tooltip'
|
||||||
import * as Switch from '@radix-ui/react-switch'
|
import * as Switch from '@radix-ui/react-switch'
|
||||||
|
|
||||||
import ArrowIcon from '~public/icons/Arrow.svg'
|
import ChevronIcon from '~public/icons/Chevron.svg'
|
||||||
import LinkIcon from '~public/icons/Link.svg'
|
import LinkIcon from '~public/icons/Link.svg'
|
||||||
import MenuIcon from '~public/icons/Menu.svg'
|
import MenuIcon from '~public/icons/Menu.svg'
|
||||||
import RemixIcon from '~public/icons/Remix.svg'
|
import RemixIcon from '~public/icons/Remix.svg'
|
||||||
|
|
@ -411,7 +411,7 @@ const Header = () => {
|
||||||
<Button
|
<Button
|
||||||
className={classNames({ Active: rightMenuOpen })}
|
className={classNames({ Active: rightMenuOpen })}
|
||||||
leftAccessoryIcon={profileImage()}
|
leftAccessoryIcon={profileImage()}
|
||||||
rightAccessoryIcon={<ArrowIcon />}
|
rightAccessoryIcon={<ChevronIcon />}
|
||||||
rightAccessoryClassName="Arrow"
|
rightAccessoryClassName="Arrow"
|
||||||
onClick={handleRightMenuButtonClicked}
|
onClick={handleRightMenuButtonClicked}
|
||||||
blended={true}
|
blended={true}
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import Select from '~components/common/Select'
|
|
||||||
import SelectItem from '~components/common/SelectItem'
|
|
||||||
import SelectGroup from '~components/common/SelectGroup'
|
|
||||||
|
|
||||||
import api from '~utils/api'
|
|
||||||
import organizeRaids from '~utils/organizeRaids'
|
|
||||||
import { appState } from '~utils/appState'
|
|
||||||
import { raidGroups } from '~data/raidGroups'
|
|
||||||
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
// Props
|
|
||||||
interface Props {
|
|
||||||
showAllRaidsOption: boolean
|
|
||||||
currentRaid?: string
|
|
||||||
defaultRaid?: string
|
|
||||||
onChange?: (slug?: string) => void
|
|
||||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up empty raid for "All raids"
|
|
||||||
const allRaidsOption = {
|
|
||||||
id: '0',
|
|
||||||
name: {
|
|
||||||
en: 'All raids',
|
|
||||||
ja: '全て',
|
|
||||||
},
|
|
||||||
slug: 'all',
|
|
||||||
level: 0,
|
|
||||||
group: 0,
|
|
||||||
element: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|
||||||
function useFieldSet(props, ref) {
|
|
||||||
// Set up router for locale
|
|
||||||
const router = useRouter()
|
|
||||||
const locale = router.locale || 'en'
|
|
||||||
|
|
||||||
// Set up local states for storing raids
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [currentRaid, setCurrentRaid] = useState<Raid | undefined>(undefined)
|
|
||||||
const [raids, setRaids] = useState<Raid[]>()
|
|
||||||
const [sortedRaids, setSortedRaids] = useState<Raid[][]>()
|
|
||||||
|
|
||||||
function openRaidSelect() {
|
|
||||||
setOpen(!open)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Organize raids into groups on mount
|
|
||||||
const organizeAllRaids = useCallback(
|
|
||||||
(raids: Raid[]) => {
|
|
||||||
let { sortedRaids } = organizeRaids(raids)
|
|
||||||
|
|
||||||
if (props.showAllRaidsOption) {
|
|
||||||
raids.unshift(allRaidsOption)
|
|
||||||
sortedRaids[0].unshift(allRaidsOption)
|
|
||||||
}
|
|
||||||
|
|
||||||
setRaids(raids)
|
|
||||||
setSortedRaids(sortedRaids)
|
|
||||||
appState.raids = raids
|
|
||||||
},
|
|
||||||
[props.showAllRaidsOption]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fetch all raids on mount
|
|
||||||
useEffect(() => {
|
|
||||||
api.endpoints.raids
|
|
||||||
.getAll()
|
|
||||||
.then((response) => organizeAllRaids(response.data))
|
|
||||||
}, [organizeRaids])
|
|
||||||
|
|
||||||
// Set current raid on mount
|
|
||||||
useEffect(() => {
|
|
||||||
if (raids && props.currentRaid) {
|
|
||||||
const raid = raids.find((raid) => raid.slug === props.currentRaid)
|
|
||||||
if (raid) setCurrentRaid(raid)
|
|
||||||
}
|
|
||||||
}, [raids, props.currentRaid])
|
|
||||||
|
|
||||||
// Enable changing select value
|
|
||||||
function handleChange(value: string) {
|
|
||||||
if (props.onChange) props.onChange(value)
|
|
||||||
|
|
||||||
if (raids) {
|
|
||||||
const raid = raids.find((raid) => raid.slug === 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) => {
|
|
||||||
if (a.element > 0 && b.element > 0) return a.element - b.element
|
|
||||||
else if (a.name.en.includes('NM') && b.name.en.includes('NM'))
|
|
||||||
return a.level < b.level ? -1 : 1
|
|
||||||
else return a.name.en < b.name.en ? -1 : 1
|
|
||||||
})
|
|
||||||
.map((item, i) => (
|
|
||||||
<SelectItem key={i} value={item.slug}>
|
|
||||||
{item.name[locale]}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectGroup
|
|
||||||
key={index}
|
|
||||||
label={raidGroups[index].name[locale]}
|
|
||||||
separator={false}
|
|
||||||
>
|
|
||||||
{options}
|
|
||||||
</SelectGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Select
|
|
||||||
value={props.currentRaid}
|
|
||||||
placeholder={'Select a raid...'}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={() => setOpen(!open)}
|
|
||||||
onClick={openRaidSelect}
|
|
||||||
onValueChange={handleChange}
|
|
||||||
>
|
|
||||||
{Array.from(Array(sortedRaids?.length)).map((x, i) =>
|
|
||||||
renderRaidGroup(i)
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default RaidDropdown
|
|
||||||
22
components/RaidSelect/index.scss
Normal file
22
components/RaidSelect/index.scss
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
.Raid.Select {
|
||||||
|
min-width: 420px;
|
||||||
|
|
||||||
|
.Top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit 0;
|
||||||
|
|
||||||
|
.SegmentedControl {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input.Bound {
|
||||||
|
background-color: var(--select-contained-bg);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--select-contained-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
170
components/RaidSelect/index.tsx
Normal file
170
components/RaidSelect/index.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import * as RadixSelect from '@radix-ui/react-select'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import Overlay from '~components/common/Overlay'
|
||||||
|
|
||||||
|
import ChevronIcon from '~public/icons/Chevron.svg'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
import SegmentedControl from '~components/common/SegmentedControl'
|
||||||
|
import Segment from '~components/common/Segment'
|
||||||
|
import Input from '~components/common/Input'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
|
HTMLSelectElement
|
||||||
|
> {
|
||||||
|
altText?: string
|
||||||
|
currentSegment: number
|
||||||
|
iconSrc?: string
|
||||||
|
open: boolean
|
||||||
|
trigger?: React.ReactNode
|
||||||
|
children?: React.ReactNode
|
||||||
|
onOpenChange?: () => void
|
||||||
|
onValueChange?: (value: string) => void
|
||||||
|
onSegmentClick: (segment: number) => void
|
||||||
|
onClose?: () => void
|
||||||
|
triggerClass?: string
|
||||||
|
overlayVisible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const RaidSelect = React.forwardRef<HTMLButtonElement, Props>(function Select(
|
||||||
|
props: Props,
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
// Import translations
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
const searchInput = React.createRef<HTMLInputElement>()
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [value, setValue] = useState('')
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
|
||||||
|
const triggerClasses = classNames(
|
||||||
|
{
|
||||||
|
SelectTrigger: true,
|
||||||
|
Disabled: props.disabled,
|
||||||
|
},
|
||||||
|
props.triggerClass
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(props.open)
|
||||||
|
}, [props.open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.value && props.value !== '') setValue(`${props.value}`)
|
||||||
|
else setValue('')
|
||||||
|
}, [props.value])
|
||||||
|
|
||||||
|
function onValueChange(newValue: string) {
|
||||||
|
setValue(`${newValue}`)
|
||||||
|
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={triggerClasses}
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
ref={forwardedRef}
|
||||||
|
>
|
||||||
|
{props.iconSrc ? <img alt={props.altText} src={props.iconSrc} /> : ''}
|
||||||
|
<RadixSelect.Value placeholder={props.placeholder} />
|
||||||
|
{!props.disabled ? (
|
||||||
|
<RadixSelect.Icon className="SelectIcon">
|
||||||
|
<ChevronIcon />
|
||||||
|
</RadixSelect.Icon>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</RadixSelect.Trigger>
|
||||||
|
|
||||||
|
<RadixSelect.Portal className="Select">
|
||||||
|
<>
|
||||||
|
<Overlay
|
||||||
|
open={open}
|
||||||
|
visible={props.overlayVisible != null ? props.overlayVisible : true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadixSelect.Content
|
||||||
|
className="Raid Select"
|
||||||
|
onCloseAutoFocus={onCloseAutoFocus}
|
||||||
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
|
onPointerDownOutside={onPointerDownOutside}
|
||||||
|
>
|
||||||
|
<div className="Top">
|
||||||
|
<Input
|
||||||
|
autoComplete="off"
|
||||||
|
className="Search Bound"
|
||||||
|
name="query"
|
||||||
|
placeholder={t('search.placeholders.raid')}
|
||||||
|
ref={searchInput}
|
||||||
|
value={query}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
<SegmentedControl blended={true}>
|
||||||
|
<Segment
|
||||||
|
groupName="raid_section"
|
||||||
|
name="events"
|
||||||
|
selected={props.currentSegment === 1}
|
||||||
|
onClick={() => props.onSegmentClick(1)}
|
||||||
|
>
|
||||||
|
{t('raids.sections.events')}
|
||||||
|
</Segment>
|
||||||
|
<Segment
|
||||||
|
groupName="raid_section"
|
||||||
|
name="raids"
|
||||||
|
selected={props.currentSegment === 0}
|
||||||
|
onClick={() => props.onSegmentClick(0)}
|
||||||
|
>
|
||||||
|
{t('raids.sections.raids')}
|
||||||
|
</Segment>
|
||||||
|
<Segment
|
||||||
|
groupName="raid_section"
|
||||||
|
name="solo"
|
||||||
|
selected={props.currentSegment === 2}
|
||||||
|
onClick={() => props.onSegmentClick(2)}
|
||||||
|
>
|
||||||
|
{t('raids.sections.solo')}
|
||||||
|
</Segment>
|
||||||
|
</SegmentedControl>
|
||||||
|
</div>
|
||||||
|
<RadixSelect.Viewport>{props.children}</RadixSelect.Viewport>
|
||||||
|
</RadixSelect.Content>
|
||||||
|
</>
|
||||||
|
</RadixSelect.Portal>
|
||||||
|
</RadixSelect.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
RaidSelect.defaultProps = {
|
||||||
|
overlayVisible: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RaidSelect
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
.Button {
|
.Button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--button-bg);
|
background: var(--button-bg);
|
||||||
border: none;
|
border: 2px solid transparent;
|
||||||
border-radius: $input-corner;
|
border-radius: $input-corner;
|
||||||
color: var(--button-text);
|
color: var(--button-text);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
.Limited {
|
|
||||||
$offset: 2px;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
background: var(--input-bg);
|
|
||||||
border-radius: $input-corner;
|
|
||||||
border: $offset solid transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
gap: $unit;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
padding-right: calc($unit-2x - $offset);
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
border: $offset solid $blue;
|
|
||||||
// box-shadow: 0 2px rgba(255, 255, 255, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Counter {
|
|
||||||
color: $grey-55;
|
|
||||||
font-weight: $bold;
|
|
||||||
line-height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: $unit * 1.5 $unit-2x;
|
|
||||||
padding-left: calc($unit-2x - $offset);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -32,7 +32,7 @@ const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset className="Fieldset">
|
<fieldset className="Fieldset">
|
||||||
<div className="Limited">
|
<div className="Joined">
|
||||||
<input
|
<input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="Input"
|
className="Input"
|
||||||
|
|
|
||||||
128
components/common/Command/index.tsx
Normal file
128
components/common/Command/index.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import { Command as CommandPrimitive } from 'cmdk'
|
||||||
|
import { Dialog } from '../Dialog'
|
||||||
|
import { DialogContent, DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
const Command = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive ref={ref} className={className} {...props} />
|
||||||
|
))
|
||||||
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
|
interface CommandDialogProps extends DialogProps {}
|
||||||
|
|
||||||
|
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogContent className="DialogContent">
|
||||||
|
<Command>{children}</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandInput = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div>
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={classNames({ CommandInput: true }, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||||
|
|
||||||
|
const CommandList = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={classNames({ CommandList: true }, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName
|
||||||
|
|
||||||
|
const CommandEmpty = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty ref={ref} className="CommandEmpty" {...props} />
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||||
|
|
||||||
|
const CommandGroup = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={classNames({ CommandGroup: true }, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||||
|
|
||||||
|
const CommandSeparator = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={classNames({ CommandSeparator: true }, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const CommandItem = forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={classNames({ CommandItem: true }, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const CommandShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames({ CommandShortcut: true }, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CommandShortcut.displayName = 'CommandShortcut'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
30
components/common/Popover/index.scss
Normal file
30
components/common/Popover/index.scss
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
.Popover {
|
||||||
|
animation: scaleIn $duration-zoom ease-out;
|
||||||
|
background: var(--dialog-bg);
|
||||||
|
border-radius: $card-corner;
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
|
||||||
|
outline: none;
|
||||||
|
padding: $unit;
|
||||||
|
transform-origin: var(--radix-popover-content-transform-origin);
|
||||||
|
width: var(--radix-popover-trigger-width);
|
||||||
|
min-width: 440px;
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Flush {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Arrow {
|
||||||
|
fill: var(--dialog-bg);
|
||||||
|
filter: drop-shadow(0px 1px 1px rgb(0 0 0 / 0.18));
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-radix-popper-content-wrapper] {
|
||||||
|
}
|
||||||
106
components/common/Popover/index.tsx
Normal file
106
components/common/Popover/index.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
PropsWithChildren,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||||
|
import ChevronIcon from '~public/icons/Chevron.svg'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props extends ComponentProps<'div'> {
|
||||||
|
open: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
icon?: {
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
trigger?: {
|
||||||
|
className?: string
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
value?: {
|
||||||
|
element: ReactNode
|
||||||
|
rawValue: string
|
||||||
|
}
|
||||||
|
onOpenChange?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Popover = React.forwardRef<HTMLDivElement, Props>(function Popover(
|
||||||
|
{ children, ...props }: PropsWithChildren<Props>,
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
// Component state
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
// Element classes
|
||||||
|
const triggerClasses = classNames(
|
||||||
|
{
|
||||||
|
SelectTrigger: true,
|
||||||
|
Disabled: props.disabled,
|
||||||
|
},
|
||||||
|
props.trigger?.className
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(props.open)
|
||||||
|
}, [props.open])
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
const value = props.value ? (
|
||||||
|
<span className="Value" data-value={props.value?.rawValue}>
|
||||||
|
{props.value?.element}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="Value Empty">{props.placeholder}</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
const icon = props.icon ? (
|
||||||
|
<img alt={props.icon.alt} src={props.icon.src} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const arrow = !props.disabled ? (
|
||||||
|
<i className="SelectIcon">
|
||||||
|
<ChevronIcon />
|
||||||
|
</i>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Root
|
||||||
|
open={open}
|
||||||
|
onOpenChange={props.onOpenChange}
|
||||||
|
modal={true}
|
||||||
|
>
|
||||||
|
<PopoverPrimitive.Trigger
|
||||||
|
className={triggerClasses}
|
||||||
|
data-placeholder={!props.value}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{value}
|
||||||
|
{arrow}
|
||||||
|
</PopoverPrimitive.Trigger>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
className={classNames({ Popover: true }, props.className)}
|
||||||
|
sideOffset={6}
|
||||||
|
ref={forwardedRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
Popover.defaultProps = {
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Popover
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
.Popover {
|
|
||||||
animation: scaleIn $duration-zoom ease-out;
|
|
||||||
background: var(--dialog-bg);
|
|
||||||
border-radius: $card-corner;
|
|
||||||
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
|
|
||||||
outline: none;
|
|
||||||
padding: $unit;
|
|
||||||
transform-origin: var(--radix-popover-content-transform-origin);
|
|
||||||
z-index: 5;
|
|
||||||
|
|
||||||
.Arrow {
|
|
||||||
fill: var(--dialog-bg);
|
|
||||||
filter: drop-shadow(0px 1px 1px rgb(0 0 0 / 0.18));
|
|
||||||
margin-top: -1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
.Segment {
|
.Segment {
|
||||||
color: $grey-55;
|
color: $grey-55;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-grow: 1;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: $normal;
|
font-weight: $normal;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,20 @@ interface Props {
|
||||||
groupName: string
|
groupName: string
|
||||||
name: string
|
name: string
|
||||||
selected: boolean
|
selected: boolean
|
||||||
|
tabIndex?: number
|
||||||
children: string
|
children: string
|
||||||
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
|
onClick: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Segment: React.FC<Props> = (props: Props) => {
|
const Segment: React.FC<Props> = (props: Props) => {
|
||||||
|
// Selects the segment when the user presses the spacebar
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLLabelElement>) => {
|
||||||
|
if (event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.currentTarget.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Segment">
|
<div className="Segment">
|
||||||
<input
|
<input
|
||||||
|
|
@ -21,7 +30,13 @@ const Segment: React.FC<Props> = (props: Props) => {
|
||||||
checked={props.selected}
|
checked={props.selected}
|
||||||
onChange={props.onClick}
|
onChange={props.onClick}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={props.name}>{props.children}</label>
|
<label
|
||||||
|
htmlFor={props.name}
|
||||||
|
tabIndex={props.tabIndex}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,15 @@
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.Blended {
|
||||||
|
background: var(--input-bound-bg);
|
||||||
|
border-radius: $full-corner;
|
||||||
|
|
||||||
|
.Segment input:checked + label {
|
||||||
|
background: var(--card-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.fire {
|
&.fire {
|
||||||
.Segment input:checked + label {
|
.Segment input:checked + label {
|
||||||
background: var(--fire-bg);
|
background: var(--fire-bg);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
className?: string
|
||||||
elementClass?: string
|
elementClass?: string
|
||||||
|
blended?: boolean
|
||||||
|
tabIndex?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SegmentedControl: React.FC<Props> = ({ elementClass, children }) => {
|
const SegmentedControl: React.FC<Props> = ({
|
||||||
|
className,
|
||||||
|
elementClass,
|
||||||
|
blended,
|
||||||
|
tabIndex,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const classes = classNames(
|
||||||
|
{
|
||||||
|
SegmentedControl: true,
|
||||||
|
Blended: blended,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
elementClass
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<div className="SegmentedControlWrapper">
|
<div className="SegmentedControlWrapper" tabIndex={tabIndex}>
|
||||||
<div className={`SegmentedControl ${elementClass ? elementClass : ''}`}>
|
<div className={classes}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SegmentedControl.defaultProps = {
|
||||||
|
blended: false,
|
||||||
|
}
|
||||||
|
|
||||||
export default SegmentedControl
|
export default SegmentedControl
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
border-radius: $input-corner;
|
border-radius: $input-corner;
|
||||||
border: none;
|
border: 2px solid transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $unit;
|
gap: $unit;
|
||||||
padding: ($unit * 1.5) $unit-2x;
|
padding: ($unit * 1.5) $unit-2x;
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-placeholder] > span:not(.SelectIcon) {
|
&[data-placeholder='true'] > span:not(.SelectIcon) {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,13 +73,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Select {
|
.Select {
|
||||||
background: var(--select-bg);
|
background: var(--dialog-bg);
|
||||||
border-radius: $input-corner;
|
border-radius: $card-corner;
|
||||||
border: $hover-stroke;
|
border: $hover-stroke;
|
||||||
box-shadow: $hover-shadow;
|
box-shadow: $hover-shadow;
|
||||||
padding: 0 $unit;
|
padding: 0 $unit;
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
|
min-width: var(--radix-select-trigger-width);
|
||||||
.Scroll.Up,
|
.Scroll.Up,
|
||||||
.Scroll.Down {
|
.Scroll.Down {
|
||||||
padding: $unit 0;
|
padding: $unit 0;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames'
|
||||||
|
|
||||||
import Overlay from '~components/common/Overlay'
|
import Overlay from '~components/common/Overlay'
|
||||||
|
|
||||||
import ArrowIcon from '~public/icons/Arrow.svg'
|
import ChevronIcon from '~public/icons/Chevron.svg'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
|
||||||
<RadixSelect.Value placeholder={props.placeholder} />
|
<RadixSelect.Value placeholder={props.placeholder} />
|
||||||
{!props.disabled ? (
|
{!props.disabled ? (
|
||||||
<RadixSelect.Icon className="SelectIcon">
|
<RadixSelect.Icon className="SelectIcon">
|
||||||
<ArrowIcon />
|
<ChevronIcon />
|
||||||
</RadixSelect.Icon>
|
</RadixSelect.Icon>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
|
|
@ -102,16 +102,18 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
|
||||||
|
|
||||||
<RadixSelect.Content
|
<RadixSelect.Content
|
||||||
className="Select"
|
className="Select"
|
||||||
|
position="popper"
|
||||||
|
sideOffset={6}
|
||||||
onCloseAutoFocus={onCloseAutoFocus}
|
onCloseAutoFocus={onCloseAutoFocus}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onPointerDownOutside={onPointerDownOutside}
|
onPointerDownOutside={onPointerDownOutside}
|
||||||
>
|
>
|
||||||
<RadixSelect.ScrollUpButton className="Scroll Up">
|
<RadixSelect.ScrollUpButton className="Scroll Up">
|
||||||
<ArrowIcon />
|
<ChevronIcon />
|
||||||
</RadixSelect.ScrollUpButton>
|
</RadixSelect.ScrollUpButton>
|
||||||
<RadixSelect.Viewport>{props.children}</RadixSelect.Viewport>
|
<RadixSelect.Viewport>{props.children}</RadixSelect.Viewport>
|
||||||
<RadixSelect.ScrollDownButton className="Scroll Down">
|
<RadixSelect.ScrollDownButton className="Scroll Down">
|
||||||
<ArrowIcon />
|
<ChevronIcon />
|
||||||
</RadixSelect.ScrollDownButton>
|
</RadixSelect.ScrollDownButton>
|
||||||
</RadixSelect.Content>
|
</RadixSelect.Content>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
font-size: $font-regular;
|
font-size: $font-regular;
|
||||||
padding: ($unit * 1.5) $unit-2x;
|
padding: ($unit * 1.5) $unit-2x;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
background-color: var(--option-bg-hover);
|
background-color: var(--option-bg-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ const SelectItem = React.forwardRef<HTMLDivElement, Props>(function selectItem(
|
||||||
const { altText, iconSrc, ...rest } = props
|
const { altText, iconSrc, ...rest } = props
|
||||||
return (
|
return (
|
||||||
<Select.Item
|
<Select.Item
|
||||||
className={classNames('SelectItem', props.className)}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
|
className={classNames({ SelectItem: true }, props.className)}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
value={`${value}`}
|
value={`${value}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
animation: scaleIn $duration-zoom ease-out;
|
animation: scaleIn $duration-zoom ease-out;
|
||||||
background: var(--dialog-bg);
|
background: var(--dialog-bg);
|
||||||
border-radius: $input-corner;
|
border-radius: $input-corner;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
font-size: $font-tiny;
|
font-size: $font-tiny;
|
||||||
font-weight: $medium;
|
font-weight: $medium;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import './index.scss'
|
||||||
interface Props {
|
interface Props {
|
||||||
new?: boolean
|
new?: boolean
|
||||||
team?: Party
|
team?: Party
|
||||||
raids: Raid[][]
|
|
||||||
selectedTab: GridType
|
selectedTab: GridType
|
||||||
pushHistory?: (path: string) => void
|
pushHistory?: (path: string) => void
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import Button from '~components/common/Button'
|
||||||
import CharLimitedFieldset from '~components/common/CharLimitedFieldset'
|
import CharLimitedFieldset from '~components/common/CharLimitedFieldset'
|
||||||
import DurationInput from '~components/common/DurationInput'
|
import DurationInput from '~components/common/DurationInput'
|
||||||
import Input from '~components/common/Input'
|
import Input from '~components/common/Input'
|
||||||
import RaidDropdown from '~components/RaidDropdown'
|
import RaidCombobox from '~components/raids/RaidCombobox'
|
||||||
import Switch from '~components/common/Switch'
|
import Switch from '~components/common/Switch'
|
||||||
import Tooltip from '~components/common/Tooltip'
|
import Tooltip from '~components/common/Tooltip'
|
||||||
import Token from '~components/common/Token'
|
import Token from '~components/common/Token'
|
||||||
|
|
@ -227,8 +227,8 @@ const PartyHeader = (props: Props) => {
|
||||||
setOpen(!open)
|
setOpen(!open)
|
||||||
}
|
}
|
||||||
|
|
||||||
function receiveRaid(slug?: string) {
|
function receiveRaid(raid?: Raid) {
|
||||||
if (slug) setRaidSlug(slug)
|
if (raid) setRaidSlug(raid?.slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchValue(value: boolean) {
|
function switchValue(value: boolean) {
|
||||||
|
|
@ -260,7 +260,8 @@ const PartyHeader = (props: Props) => {
|
||||||
|
|
||||||
function updateDetails(event: React.MouseEvent) {
|
function updateDetails(event: React.MouseEvent) {
|
||||||
const descriptionValue = descriptionInput.current?.value
|
const descriptionValue = descriptionInput.current?.value
|
||||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
const allRaids = appState.raidGroups.flatMap((group) => group.raids)
|
||||||
|
const raid = allRaids.find((raid) => raid.slug === raidSlug)
|
||||||
|
|
||||||
const details: DetailsObject = {
|
const details: DetailsObject = {
|
||||||
fullAuto: fullAuto,
|
fullAuto: fullAuto,
|
||||||
|
|
@ -498,9 +499,9 @@ const PartyHeader = (props: Props) => {
|
||||||
error={errors.name}
|
error={errors.name}
|
||||||
ref={nameInput}
|
ref={nameInput}
|
||||||
/>
|
/>
|
||||||
<RaidDropdown
|
<RaidCombobox
|
||||||
showAllRaidsOption={false}
|
showAllRaidsOption={false}
|
||||||
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
|
currentRaid={props.party?.raid ? props.party?.raid : undefined}
|
||||||
onChange={receiveRaid}
|
onChange={receiveRaid}
|
||||||
/>
|
/>
|
||||||
<ul className="SwitchToggleGroup DetailToggleGroup">
|
<ul className="SwitchToggleGroup DetailToggleGroup">
|
||||||
|
|
@ -650,7 +651,7 @@ const PartyHeader = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="attribution">
|
<div className="attribution">
|
||||||
{renderUserBlock()}
|
{renderUserBlock()}
|
||||||
{party.raid ? linkedRaidBlock(party.raid) : ''}
|
{appState.party.raid ? linkedRaidBlock(appState.party.raid) : ''}
|
||||||
{party.created_at != '' ? (
|
{party.created_at != '' ? (
|
||||||
<time
|
<time
|
||||||
className="last-updated"
|
className="last-updated"
|
||||||
|
|
|
||||||
197
components/raids/RaidCombobox/index.scss
Normal file
197
components/raids/RaidCombobox/index.scss
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
.Combobox.Raid {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
background: var(--dialog-bg);
|
||||||
|
border-top-left-radius: $card-corner;
|
||||||
|
border-top-right-radius: $card-corner;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.Clear.Button {
|
||||||
|
background: none;
|
||||||
|
padding: ($unit * 0.75) $unit-half $unit-half;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:hover svg {
|
||||||
|
fill: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--text-tertiary);
|
||||||
|
width: $unit-2x;
|
||||||
|
height: $unit-2x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Controls {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
.Button.Blended.small {
|
||||||
|
padding: $unit ($unit * 1.25);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-contained-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Flipped {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SegmentedControlWrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.SegmentedControl {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raids {
|
||||||
|
border-bottom-left-radius: $card-corner;
|
||||||
|
border-bottom-right-radius: $card-corner;
|
||||||
|
height: 50vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 0 $unit;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
height: 28vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Searching {
|
||||||
|
.CommandGroup {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
|
.Label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectItem {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CommandGroup.Hidden {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CommandGroup {
|
||||||
|
&.Hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Label {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: $font-small;
|
||||||
|
font-weight: $medium;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit $unit-2x $unit-half ($unit * 1.5);
|
||||||
|
|
||||||
|
.Separator {
|
||||||
|
background: var(--select-separator);
|
||||||
|
border-radius: 1px;
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.DetailsWrapper .PartyDetails.Editable .Raid.SelectTrigger {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 11px;
|
||||||
|
min-height: 51px;
|
||||||
|
|
||||||
|
.Value {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-half;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.Info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtraIndicator {
|
||||||
|
background: var(--extra-purple-bg);
|
||||||
|
border-radius: $full-corner;
|
||||||
|
color: var(--extra-purple-text);
|
||||||
|
display: flex;
|
||||||
|
font-weight: $bold;
|
||||||
|
font-size: $font-tiny;
|
||||||
|
width: $unit-3x;
|
||||||
|
height: $unit-3x;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Group,
|
||||||
|
.Separator {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid.wind {
|
||||||
|
color: var(--wind-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid.fire {
|
||||||
|
color: var(--fire-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid.water {
|
||||||
|
color: var(--water-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid.earth {
|
||||||
|
color: var(--earth-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid.dark {
|
||||||
|
color: var(--dark-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid.light {
|
||||||
|
color: var(--light-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Filters .SelectTrigger.Raid {
|
||||||
|
& > span {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Raid {
|
||||||
|
display: block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
568
components/raids/RaidCombobox/index.tsx
Normal file
568
components/raids/RaidCombobox/index.tsx
Normal file
|
|
@ -0,0 +1,568 @@
|
||||||
|
import { createRef, useCallback, useEffect, useState, useRef } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import { Command, CommandGroup, CommandInput } from 'cmdk'
|
||||||
|
import Popover from '~components/common/Popover'
|
||||||
|
import SegmentedControl from '~components/common/SegmentedControl'
|
||||||
|
import Segment from '~components/common/Segment'
|
||||||
|
import RaidItem from '~components/raids/RaidItem'
|
||||||
|
import Tooltip from '~components/common/Tooltip'
|
||||||
|
|
||||||
|
import api from '~utils/api'
|
||||||
|
import { appState } from '~utils/appState'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showAllRaidsOption: boolean
|
||||||
|
currentRaid?: Raid
|
||||||
|
defaultRaid?: Raid
|
||||||
|
minimal?: boolean
|
||||||
|
onChange?: (raid?: Raid) => void
|
||||||
|
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
import Button from '~components/common/Button'
|
||||||
|
import ArrowIcon from '~public/icons/Arrow.svg'
|
||||||
|
import CrossIcon from '~public/icons/Cross.svg'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
const NUM_SECTIONS = 3
|
||||||
|
const NUM_ELEMENTS = 5
|
||||||
|
|
||||||
|
enum Sort {
|
||||||
|
ASCENDING,
|
||||||
|
DESCENDING,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up empty raid for "All raids"
|
||||||
|
const untitledGroup: RaidGroup = {
|
||||||
|
id: '0',
|
||||||
|
name: {
|
||||||
|
en: '',
|
||||||
|
ja: '',
|
||||||
|
},
|
||||||
|
section: 0,
|
||||||
|
order: 0,
|
||||||
|
extra: false,
|
||||||
|
raids: [],
|
||||||
|
difficulty: 0,
|
||||||
|
hl: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up empty raid for "All raids"
|
||||||
|
const allRaidsOption: Raid = {
|
||||||
|
id: '0',
|
||||||
|
name: {
|
||||||
|
en: 'All battles',
|
||||||
|
ja: '全てのバトル',
|
||||||
|
},
|
||||||
|
group: untitledGroup,
|
||||||
|
slug: 'all',
|
||||||
|
level: 0,
|
||||||
|
element: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const RaidCombobox = (props: Props) => {
|
||||||
|
// Set up router for locale
|
||||||
|
const router = useRouter()
|
||||||
|
const locale = router.locale || 'en'
|
||||||
|
|
||||||
|
// Set up translations
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// Component state
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [sort, setSort] = useState<Sort>(Sort.DESCENDING)
|
||||||
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
|
// Data state
|
||||||
|
const [currentSection, setCurrentSection] = useState(1)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [sections, setSections] = useState<RaidGroup[][]>()
|
||||||
|
const [currentRaid, setCurrentRaid] = useState<Raid>()
|
||||||
|
const [tabIndex, setTabIndex] = useState(NUM_ELEMENTS + 1)
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const [farmingRaid, setFarmingRaid] = useState<Raid>()
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const listRef = createRef<HTMLDivElement>()
|
||||||
|
const inputRef = createRef<HTMLInputElement>()
|
||||||
|
const sortButtonRef = createRef<HTMLButtonElement>()
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Methods: Lifecycle Hooks
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
// Fetch all raids on mount
|
||||||
|
useEffect(() => {
|
||||||
|
api.raidGroups().then((response) => sortGroups(response.data))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Set current raid and section when the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (appState.party.raid) {
|
||||||
|
setCurrentRaid(appState.party.raid)
|
||||||
|
setCurrentSection(appState.party.raid.group.section)
|
||||||
|
} else if (props.showAllRaidsOption && !currentRaid) {
|
||||||
|
setCurrentRaid(allRaidsOption)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Set current raid and section when the current raid changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.currentRaid) {
|
||||||
|
console.log('We are here with a raid')
|
||||||
|
setCurrentRaid(props.currentRaid)
|
||||||
|
setCurrentSection(props.currentRaid.group.section)
|
||||||
|
}
|
||||||
|
}, [props.currentRaid])
|
||||||
|
|
||||||
|
// Scroll to the top of the list when the user switches tabs
|
||||||
|
useEffect(() => {
|
||||||
|
if (listRef.current) {
|
||||||
|
listRef.current.scrollTop = 0
|
||||||
|
}
|
||||||
|
}, [currentSection])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTabIndex(NUM_ELEMENTS + 1)
|
||||||
|
}, [currentSection])
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Methods: Event Handlers
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
// Handle Escape key press event
|
||||||
|
const handleEscapeKeyPressed = useCallback(() => {
|
||||||
|
if (listRef.current) {
|
||||||
|
listRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [open, currentRaid, sortButtonRef])
|
||||||
|
|
||||||
|
// Handle Arrow key press event by focusing the list item above or below the current one based on the direction
|
||||||
|
const handleArrowKeyPressed = useCallback(
|
||||||
|
(direction: 'Up' | 'Down') => {
|
||||||
|
const current = listRef.current?.querySelector(
|
||||||
|
'.Raid:focus'
|
||||||
|
) as HTMLElement | null
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
let next: Element | null | undefined
|
||||||
|
|
||||||
|
if (direction === 'Down' && !current.nextElementSibling) {
|
||||||
|
const nextParent =
|
||||||
|
current.parentElement?.parentElement?.nextElementSibling
|
||||||
|
next = nextParent?.querySelector('.Raid')
|
||||||
|
} else if (direction === 'Up' && !current.previousElementSibling) {
|
||||||
|
const previousParent =
|
||||||
|
current.parentElement?.parentElement?.previousElementSibling
|
||||||
|
next = previousParent?.querySelector('.Raid:last-child')
|
||||||
|
} else {
|
||||||
|
next =
|
||||||
|
direction === 'Up'
|
||||||
|
? current.previousElementSibling
|
||||||
|
: current.nextElementSibling
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
;(next as HTMLElement).focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[open, currentRaid, listRef]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scroll to an item in the list when it is selected
|
||||||
|
const scrollToItem = useCallback(
|
||||||
|
(node) => {
|
||||||
|
if (!scrolled && open && currentRaid && listRef.current && node) {
|
||||||
|
const { top: listTop } = listRef.current.getBoundingClientRect()
|
||||||
|
const { top: itemTop } = node.getBoundingClientRect()
|
||||||
|
|
||||||
|
listRef.current.scrollTop = itemTop - listTop
|
||||||
|
console.log('Focusing node')
|
||||||
|
node.focus()
|
||||||
|
setScrolled(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrolled, open, currentRaid, listRef]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reverse the sort order
|
||||||
|
function reverseSort() {
|
||||||
|
if (sort === Sort.ASCENDING) setSort(Sort.DESCENDING)
|
||||||
|
else setSort(Sort.ASCENDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorts the raid groups into sections
|
||||||
|
const sortGroups = useCallback(
|
||||||
|
(groups: RaidGroup[]) => {
|
||||||
|
const sections: [RaidGroup[], RaidGroup[], RaidGroup[]] = [[], [], []]
|
||||||
|
|
||||||
|
groups.forEach((group) => {
|
||||||
|
if (group.section > 0) sections[group.section - 1].push(group)
|
||||||
|
})
|
||||||
|
|
||||||
|
setFarmingRaid(groups[0].raids[0])
|
||||||
|
|
||||||
|
setSections(sections)
|
||||||
|
},
|
||||||
|
[setSections]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSortButtonKeyDown = (
|
||||||
|
event: React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
// If the tab key is pressed without the Shift key, focus the raid list
|
||||||
|
if (event.key === 'Tab' && !event.shiftKey) {
|
||||||
|
if (listRef.current) {
|
||||||
|
listRef.current.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === 'Tab' && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Tab' && event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (sortButtonRef.current) {
|
||||||
|
sortButtonRef.current.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the enter key is pressed, focus the first raid item in the list
|
||||||
|
else if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
if (listRef.current) {
|
||||||
|
const raid = listRef.current.querySelector('.Raid')
|
||||||
|
if (raid) {
|
||||||
|
;(raid as HTMLElement).focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle value change for the raid selection
|
||||||
|
function handleValueChange(raid: Raid) {
|
||||||
|
setCurrentRaid(raid)
|
||||||
|
setOpen(false)
|
||||||
|
setScrolled(false)
|
||||||
|
if (props.onChange) props.onChange(raid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the open state of the combobox
|
||||||
|
function toggleOpen() {
|
||||||
|
if (open) {
|
||||||
|
if (currentRaid && currentRaid.slug !== 'all') {
|
||||||
|
setCurrentSection(currentRaid.group.section)
|
||||||
|
}
|
||||||
|
setScrolled(false)
|
||||||
|
}
|
||||||
|
setOpen(!open)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the search query
|
||||||
|
function clearSearch() {
|
||||||
|
setQuery('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Methods: Rendering
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
// Renders each raid section
|
||||||
|
function renderRaidSections() {
|
||||||
|
return Array.from({ length: NUM_SECTIONS }, (_, i) => renderRaidSection(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the specified raid section
|
||||||
|
function renderRaidSection(section: number) {
|
||||||
|
const currentSection = sections?.[section]
|
||||||
|
if (!currentSection) return
|
||||||
|
|
||||||
|
const sortedGroups = currentSection.sort((a, b) => {
|
||||||
|
return sort === Sort.ASCENDING ? a.order - b.order : b.order - a.order
|
||||||
|
})
|
||||||
|
|
||||||
|
return sortedGroups.map((group, i) => renderRaidGroup(section, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the specified raid group
|
||||||
|
function renderRaidGroup(section: number, index: number) {
|
||||||
|
if (!sections?.[section]?.[index]) return
|
||||||
|
|
||||||
|
const group = sections[section][index]
|
||||||
|
const options = generateRaidItems(group.raids)
|
||||||
|
|
||||||
|
const groupClassName = classNames({
|
||||||
|
CommandGroup: true,
|
||||||
|
Hidden: group.section !== currentSection,
|
||||||
|
})
|
||||||
|
|
||||||
|
const heading = (
|
||||||
|
<div className="Label">
|
||||||
|
{group.name[locale]}
|
||||||
|
<div className="Separator" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandGroup
|
||||||
|
data-section={group.section}
|
||||||
|
className={groupClassName}
|
||||||
|
key={group.name[locale].toLowerCase().replace(' ', '-')}
|
||||||
|
heading={heading}
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</CommandGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the ungrouped raid group
|
||||||
|
function renderUngroupedRaids() {
|
||||||
|
let ungroupedRaids = farmingRaid ? [farmingRaid] : []
|
||||||
|
|
||||||
|
if (props.showAllRaidsOption) {
|
||||||
|
ungroupedRaids.push(allRaidsOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = generateRaidItems(ungroupedRaids)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandGroup
|
||||||
|
data-section={untitledGroup.section}
|
||||||
|
className={classNames({
|
||||||
|
CommandGroup: true,
|
||||||
|
})}
|
||||||
|
key="ungrouped-raids"
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</CommandGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a list of RaidItem components from the specified raids
|
||||||
|
function generateRaidItems(raids: Raid[]) {
|
||||||
|
return raids
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.element > 0 && b.element > 0) return a.element - b.element
|
||||||
|
if (a.name.en.includes('NM') && b.name.en.includes('NM'))
|
||||||
|
return a.level - b.level
|
||||||
|
return a.name.en.localeCompare(b.name.en)
|
||||||
|
})
|
||||||
|
.map((item, i) => renderRaidItem(item, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders a RaidItem component for the specified raid
|
||||||
|
function renderRaidItem(raid: Raid, key: number) {
|
||||||
|
const isSelected = currentRaid?.id === raid.id
|
||||||
|
const isRef = isSelected ? scrollToItem : undefined
|
||||||
|
const imageUrl = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/raids/${raid.slug}.png`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidItem
|
||||||
|
className={isSelected ? 'Selected' : ''}
|
||||||
|
icon={{ alt: raid.name[locale], src: imageUrl }}
|
||||||
|
extra={raid.group.extra}
|
||||||
|
key={key}
|
||||||
|
selected={isSelected}
|
||||||
|
ref={isRef}
|
||||||
|
role="listitem"
|
||||||
|
tabIndex={0}
|
||||||
|
value={raid.slug}
|
||||||
|
onEscapeKeyPressed={handleEscapeKeyPressed}
|
||||||
|
onArrowKeyPressed={handleArrowKeyPressed}
|
||||||
|
onSelect={() => handleValueChange(raid)}
|
||||||
|
>
|
||||||
|
{raid.name[locale]}
|
||||||
|
</RaidItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders a SegmentedControl component for selecting raid sections.
|
||||||
|
function renderSegmentedControl() {
|
||||||
|
return (
|
||||||
|
<SegmentedControl blended={true}>
|
||||||
|
<Segment
|
||||||
|
groupName="raid_section"
|
||||||
|
name="events"
|
||||||
|
selected={currentSection === 2}
|
||||||
|
tabIndex={2}
|
||||||
|
onClick={() => setCurrentSection(2)}
|
||||||
|
>
|
||||||
|
{t('raids.sections.events')}
|
||||||
|
</Segment>
|
||||||
|
<Segment
|
||||||
|
groupName="raid_section"
|
||||||
|
name="raids"
|
||||||
|
selected={currentSection === 1}
|
||||||
|
tabIndex={3}
|
||||||
|
onClick={() => setCurrentSection(1)}
|
||||||
|
>
|
||||||
|
{t('raids.sections.raids')}
|
||||||
|
</Segment>
|
||||||
|
<Segment
|
||||||
|
groupName="raid_section"
|
||||||
|
name="solo"
|
||||||
|
selected={currentSection === 3}
|
||||||
|
tabIndex={4}
|
||||||
|
onClick={() => setCurrentSection(3)}
|
||||||
|
>
|
||||||
|
{t('raids.sections.solo')}
|
||||||
|
</Segment>
|
||||||
|
</SegmentedControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders a Button for sorting raids and a Tooltip for explaining what it does.
|
||||||
|
function renderSortButton() {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
sort === Sort.ASCENDING
|
||||||
|
? 'Lower difficulty battles first'
|
||||||
|
: 'Higher difficulty battles first'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
blended={true}
|
||||||
|
buttonSize="small"
|
||||||
|
leftAccessoryIcon={<ArrowIcon />}
|
||||||
|
leftAccessoryClassName={sort === Sort.DESCENDING ? 'Flipped' : ''}
|
||||||
|
onClick={reverseSort}
|
||||||
|
onKeyDown={handleSortButtonKeyDown}
|
||||||
|
ref={sortButtonRef}
|
||||||
|
tabIndex={5}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the content for the Popover trigger.
|
||||||
|
function renderTriggerContent() {
|
||||||
|
if (currentRaid) {
|
||||||
|
const element = (
|
||||||
|
<>
|
||||||
|
{!props.minimal ? (
|
||||||
|
<div className="Info">
|
||||||
|
<span className="Group">{currentRaid.group.name[locale]}</span>
|
||||||
|
<span className="Separator">/</span>
|
||||||
|
<span className={classNames({ Raid: true }, linkClass)}>
|
||||||
|
{currentRaid.name[locale]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className={classNames({ Raid: true }, linkClass)}>
|
||||||
|
{currentRaid.name[locale]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentRaid.group.extra && !props.minimal && (
|
||||||
|
<i className="ExtraIndicator">EX</i>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
rawValue: currentRaid.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the search input for the raid combobox
|
||||||
|
function renderSearchInput() {
|
||||||
|
return (
|
||||||
|
<div className="Bound Joined">
|
||||||
|
<CommandInput
|
||||||
|
className="Input"
|
||||||
|
placeholder={t('search.placeholders.raid')}
|
||||||
|
tabIndex={1}
|
||||||
|
ref={inputRef}
|
||||||
|
value={query}
|
||||||
|
onValueChange={setQuery}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
Button: true,
|
||||||
|
Clear: true,
|
||||||
|
Visible: query.length > 0,
|
||||||
|
})}
|
||||||
|
onClick={clearSearch}
|
||||||
|
>
|
||||||
|
<CrossIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Methods: Utility
|
||||||
|
// ----------------------------------------------
|
||||||
|
function slugToRaid(slug: string) {
|
||||||
|
return appState.raidGroups
|
||||||
|
.filter((group) => group.section > 0)
|
||||||
|
.flatMap((group) => group.raids)
|
||||||
|
.find((raid) => raid.slug === slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkClass = classNames({
|
||||||
|
wind: currentRaid && currentRaid.element == 1,
|
||||||
|
fire: currentRaid && currentRaid.element == 2,
|
||||||
|
water: currentRaid && currentRaid.element == 3,
|
||||||
|
earth: currentRaid && currentRaid.element == 4,
|
||||||
|
dark: currentRaid && currentRaid.element == 5,
|
||||||
|
light: currentRaid && currentRaid.element == 6,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Render
|
||||||
|
// ----------------------------------------------
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
className="Flush"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={toggleOpen}
|
||||||
|
placeholder={t('raids.placeholder')}
|
||||||
|
trigger={{ className: 'Raid' }}
|
||||||
|
value={renderTriggerContent()}
|
||||||
|
>
|
||||||
|
<Command className="Raid Combobox">
|
||||||
|
<div className="Header">
|
||||||
|
{renderSearchInput()}
|
||||||
|
{!query && (
|
||||||
|
<div className="Controls">
|
||||||
|
{renderSegmentedControl()}
|
||||||
|
{renderSortButton()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames({ Raids: true, Searching: query !== '' })}
|
||||||
|
ref={listRef}
|
||||||
|
role="listbox"
|
||||||
|
tabIndex={6}
|
||||||
|
onKeyDown={handleListKeyDown}
|
||||||
|
>
|
||||||
|
{renderUngroupedRaids()}
|
||||||
|
{renderRaidSections()}
|
||||||
|
</div>
|
||||||
|
</Command>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RaidCombobox.defaultProps = {
|
||||||
|
minimal: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RaidCombobox
|
||||||
53
components/raids/RaidItem/index.scss
Normal file
53
components/raids/RaidItem/index.scss
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
.SelectItem.Raid {
|
||||||
|
padding-top: $unit;
|
||||||
|
padding-bottom: $unit;
|
||||||
|
padding-left: $unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.ExtraIndicator {
|
||||||
|
background: var(--extra-purple-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Selected {
|
||||||
|
background-color: var(--pill-bg-hover);
|
||||||
|
color: var(--pill-text-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Text {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtraIndicator {
|
||||||
|
background: var(--extra-purple-bg);
|
||||||
|
border-radius: $full-corner;
|
||||||
|
color: var(--extra-purple-text);
|
||||||
|
display: flex;
|
||||||
|
font-weight: $bold;
|
||||||
|
font-size: $font-tiny;
|
||||||
|
width: $unit-3x;
|
||||||
|
height: $unit-3x;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Selected {
|
||||||
|
background-color: var(--pill-bg);
|
||||||
|
color: var(--pill-text);
|
||||||
|
border-radius: $full-corner;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: $font-tiny;
|
||||||
|
font-weight: $bold;
|
||||||
|
padding: 0 $unit;
|
||||||
|
height: $unit-3x;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
background: var(--input-bound-bg);
|
||||||
|
border-radius: $unit-half;
|
||||||
|
width: $unit-10x;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
components/raids/RaidItem/index.tsx
Normal file
87
components/raids/RaidItem/index.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React, { ComponentProps, PropsWithChildren } from 'react'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
import { CommandItem } from '~components/common/Command'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props extends ComponentProps<'div'> {
|
||||||
|
className?: string
|
||||||
|
icon?: {
|
||||||
|
alt: string
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
extra: boolean
|
||||||
|
selected: boolean
|
||||||
|
tabIndex?: number
|
||||||
|
value: string | number
|
||||||
|
onSelect: () => void
|
||||||
|
onArrowKeyPressed?: (direction: 'Up' | 'Down') => void
|
||||||
|
onEscapeKeyPressed?: () => void
|
||||||
|
}
|
||||||
|
const RaidItem = React.forwardRef<HTMLDivElement, PropsWithChildren<Props>>(
|
||||||
|
function Item(
|
||||||
|
{
|
||||||
|
icon,
|
||||||
|
value,
|
||||||
|
extra,
|
||||||
|
selected,
|
||||||
|
tabIndex,
|
||||||
|
children,
|
||||||
|
onEscapeKeyPressed,
|
||||||
|
onArrowKeyPressed,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<Props>,
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
const classes = classNames(
|
||||||
|
{ SelectItem: true, Raid: true },
|
||||||
|
props.className
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === 'Escape' && onEscapeKeyPressed) {
|
||||||
|
event.preventDefault()
|
||||||
|
onEscapeKeyPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault()
|
||||||
|
if (onArrowKeyPressed) {
|
||||||
|
console.log(event.key)
|
||||||
|
onArrowKeyPressed(event.key === 'ArrowUp' ? 'Up' : 'Down')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
props.onSelect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
{...props}
|
||||||
|
className={classes}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
value={`${value}`}
|
||||||
|
onClick={props.onSelect}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
ref={forwardedRef}
|
||||||
|
>
|
||||||
|
{icon ? <img alt={icon.alt} src={icon.src} /> : ''}
|
||||||
|
<span className="Text">{children}</span>
|
||||||
|
{selected ? <i className="Selected">{t('combobox.selected')}</i> : ''}
|
||||||
|
{extra ? <i className="ExtraIndicator">EX</i> : ''}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RaidItem.defaultProps = {
|
||||||
|
extra: false,
|
||||||
|
selected: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RaidItem
|
||||||
|
|
@ -2,7 +2,7 @@ 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 ChevronIcon from '~public/icons/Chevron.svg'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -22,7 +22,7 @@ const SearchFilter = (props: Props) => {
|
||||||
<span className="count">{props.numSelected}</span>
|
<span className="count">{props.numSelected}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="icon">
|
<span className="icon">
|
||||||
<ArrowIcon />
|
<ChevronIcon />
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
|
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
|
||||||
|
|
|
||||||
12971
package-lock.json
generated
12971
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
|
@ -28,6 +28,7 @@
|
||||||
"@svgr/webpack": "^6.2.0",
|
"@svgr/webpack": "^6.2.0",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"cmdk": "^0.2.0",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
|
@ -44,8 +45,8 @@
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"next-usequerystate": "^1.7.0",
|
"next-usequerystate": "^1.7.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"react": "17.0.2",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.0.0",
|
||||||
"react-i18next": "^11.15.5",
|
"react-i18next": "^11.15.5",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
|
|
@ -61,15 +62,15 @@
|
||||||
"youtube-api-v3-wrapper": "^2.3.0"
|
"youtube-api-v3-wrapper": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "^7.0.2",
|
"@storybook/addon-essentials": "latest",
|
||||||
"@storybook/addon-interactions": "^7.0.2",
|
"@storybook/addon-interactions": "latest",
|
||||||
"@storybook/addon-links": "^7.0.2",
|
"@storybook/addon-links": "latest",
|
||||||
"@storybook/addon-mdx-gfm": "^7.0.2",
|
"@storybook/addon-mdx-gfm": "latest",
|
||||||
"@storybook/addon-styling": "^0.3.2",
|
"@storybook/addon-styling": "latest",
|
||||||
"@storybook/blocks": "^7.0.2",
|
"@storybook/blocks": "latest",
|
||||||
"@storybook/nextjs": "^7.0.2",
|
"@storybook/nextjs": "latest",
|
||||||
"@storybook/react": "^7.0.2",
|
"@storybook/react": "latest",
|
||||||
"@storybook/testing-library": "^0.0.14-next.2",
|
"@storybook/testing-library": "latest",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/node": "17.0.11",
|
"@types/node": "17.0.11",
|
||||||
|
|
@ -86,7 +87,7 @@
|
||||||
"eslint-plugin-storybook": "^0.6.11",
|
"eslint-plugin-storybook": "^0.6.11",
|
||||||
"eslint-plugin-valtio": "^0.4.1",
|
"eslint-plugin-valtio": "^0.4.1",
|
||||||
"sass-loader": "^13.2.2",
|
"sass-loader": "^13.2.2",
|
||||||
"storybook": "^7.0.2",
|
"storybook": "latest",
|
||||||
"typescript": "^4.5.5"
|
"typescript": "^4.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import extractFilters from '~utils/extractFilters'
|
import extractFilters from '~utils/extractFilters'
|
||||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||||
import organizeRaids from '~utils/organizeRaids'
|
|
||||||
import { setHeaders } from '~utils/userToken'
|
import { setHeaders } from '~utils/userToken'
|
||||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
|
|
@ -356,12 +355,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch and organize raids
|
// Fetch and organize raids
|
||||||
let { raids, sortedRaids } = await api.endpoints.raids
|
let raidGroups: RaidGroup[] = await api
|
||||||
.getAll()
|
.raidGroups()
|
||||||
.then((response) => organizeRaids(response.data))
|
.then((response) => response.data)
|
||||||
|
|
||||||
// Create filter object
|
// Create filter object
|
||||||
const filters: FilterObject = extractFilters(query, raids)
|
const filters: FilterObject = extractFilters(query, raidGroups)
|
||||||
const params = {
|
const params = {
|
||||||
params: { ...filters, ...advancedFilters },
|
params: { ...filters, ...advancedFilters },
|
||||||
}
|
}
|
||||||
|
|
@ -393,8 +392,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
const context: PageContextObj = {
|
const context: PageContextObj = {
|
||||||
user: user,
|
user: user,
|
||||||
teams: teams,
|
teams: teams,
|
||||||
raids: raids,
|
raidGroups: raidGroups,
|
||||||
sortedRaids: sortedRaids,
|
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import NewHead from '~components/head/NewHead'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||||
import organizeRaids from '~utils/organizeRaids'
|
|
||||||
import { accountCookie, setHeaders } from '~utils/userToken'
|
import { accountCookie, setHeaders } from '~utils/userToken'
|
||||||
import { appState, initialAppState } from '~utils/appState'
|
import { appState, initialAppState } from '~utils/appState'
|
||||||
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
||||||
|
|
@ -69,7 +68,7 @@ const NewRoute: React.FC<Props> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (context && context.jobs && context.jobSkills) {
|
if (context && context.jobs && context.jobSkills) {
|
||||||
appState.raids = context.raids
|
appState.raids = context.raidGroups
|
||||||
appState.jobs = context.jobs
|
appState.jobs = context.jobs
|
||||||
appState.jobSkills = context.jobSkills
|
appState.jobSkills = context.jobSkills
|
||||||
appState.weaponKeys = context.weaponKeys
|
appState.weaponKeys = context.weaponKeys
|
||||||
|
|
@ -106,12 +105,7 @@ const NewRoute: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={router.asPath}>
|
<React.Fragment key={router.asPath}>
|
||||||
{pageHead()}
|
{pageHead()}
|
||||||
<Party
|
<Party new={true} pushHistory={callback} selectedTab={selectedTab} />
|
||||||
new={true}
|
|
||||||
raids={context.sortedRaids}
|
|
||||||
pushHistory={callback}
|
|
||||||
selectedTab={selectedTab}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
} else return pageError()
|
} else return pageError()
|
||||||
|
|
@ -153,9 +147,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch and organize raids
|
// Fetch and organize raids
|
||||||
let { raids, sortedRaids } = await api.endpoints.raids
|
let raidGroups: RaidGroup[] = await api.raidGroups().then((response) => response.data)
|
||||||
.getAll()
|
|
||||||
.then((response) => organizeRaids(response.data))
|
|
||||||
|
|
||||||
// Fetch jobs and job skills
|
// Fetch jobs and job skills
|
||||||
let jobs = await api.endpoints.jobs
|
let jobs = await api.endpoints.jobs
|
||||||
|
|
@ -174,8 +166,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
const context: PageContextObj = {
|
const context: PageContextObj = {
|
||||||
jobs: jobs,
|
jobs: jobs,
|
||||||
jobSkills: jobSkills,
|
jobSkills: jobSkills,
|
||||||
raids: raids,
|
raidGroups: raidGroups,
|
||||||
sortedRaids: sortedRaids,
|
|
||||||
weaponKeys: weaponKeys,
|
weaponKeys: weaponKeys,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import PartyHead from '~components/party/PartyHead'
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import elementEmoji from '~utils/elementEmoji'
|
import elementEmoji from '~utils/elementEmoji'
|
||||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||||
import organizeRaids from '~utils/organizeRaids'
|
|
||||||
import { setHeaders } from '~utils/userToken'
|
import { setHeaders } from '~utils/userToken'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
||||||
|
|
@ -57,7 +56,7 @@ const PartyRoute: React.FC<Props> = ({
|
||||||
// Set the initial data from props
|
// Set the initial data from props
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (context && !error) {
|
if (context && !error) {
|
||||||
appState.raids = context.raids
|
appState.raidGroups = context.raidGroups
|
||||||
appState.jobs = context.jobs ? context.jobs : []
|
appState.jobs = context.jobs ? context.jobs : []
|
||||||
appState.jobSkills = context.jobSkills ? context.jobSkills : []
|
appState.jobSkills = context.jobSkills ? context.jobSkills : []
|
||||||
appState.weaponKeys = context.weaponKeys
|
appState.weaponKeys = context.weaponKeys
|
||||||
|
|
@ -85,11 +84,7 @@ const PartyRoute: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={router.asPath}>
|
<React.Fragment key={router.asPath}>
|
||||||
{pageHead()}
|
{pageHead()}
|
||||||
<Party
|
<Party team={context.party} selectedTab={selectedTab} />
|
||||||
team={context.party}
|
|
||||||
raids={context.sortedRaids}
|
|
||||||
selectedTab={selectedTab}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
} else return pageError()
|
} else return pageError()
|
||||||
|
|
@ -115,9 +110,9 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch and organize raids
|
// Fetch and organize raids
|
||||||
let { raids, sortedRaids } = await api.endpoints.raids
|
let raidGroups: RaidGroup[] = await api
|
||||||
.getAll()
|
.raidGroups()
|
||||||
.then((response) => organizeRaids(response.data))
|
.then((response) => response.data)
|
||||||
|
|
||||||
// Fetch jobs and job skills
|
// Fetch jobs and job skills
|
||||||
let jobs = await api.endpoints.jobs
|
let jobs = await api.endpoints.jobs
|
||||||
|
|
@ -148,8 +143,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
party: party,
|
party: party,
|
||||||
jobs: jobs,
|
jobs: jobs,
|
||||||
jobSkills: jobSkills,
|
jobSkills: jobSkills,
|
||||||
raids: raids,
|
raidGroups: raidGroups,
|
||||||
sortedRaids: sortedRaids,
|
|
||||||
weaponKeys: weaponKeys,
|
weaponKeys: weaponKeys,
|
||||||
meta: {
|
meta: {
|
||||||
element: elementEmoji(party),
|
element: elementEmoji(party),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import api from '~utils/api'
|
||||||
import { setHeaders } from '~utils/userToken'
|
import { setHeaders } from '~utils/userToken'
|
||||||
import extractFilters from '~utils/extractFilters'
|
import extractFilters from '~utils/extractFilters'
|
||||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||||
import organizeRaids from '~utils/organizeRaids'
|
|
||||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
import { defaultFilterset } from '~utils/defaultFilters'
|
import { defaultFilterset } from '~utils/defaultFilters'
|
||||||
|
|
@ -390,12 +389,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch and organize raids
|
// Fetch and organize raids
|
||||||
let { raids, sortedRaids } = await api.endpoints.raids
|
let raidGroups: RaidGroup[] = await api
|
||||||
.getAll()
|
.raidGroups()
|
||||||
.then((response) => organizeRaids(response.data))
|
.then((response) => response.data)
|
||||||
|
|
||||||
// Create filter object
|
// Create filter object
|
||||||
const filters: FilterObject = extractFilters(query, raids)
|
const filters: FilterObject = extractFilters(query, raidGroups)
|
||||||
const params = {
|
const params = {
|
||||||
params: { ...filters, ...advancedFilters },
|
params: { ...filters, ...advancedFilters },
|
||||||
}
|
}
|
||||||
|
|
@ -416,8 +415,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
// Consolidate data into context object
|
// Consolidate data into context object
|
||||||
const context: PageContextObj = {
|
const context: PageContextObj = {
|
||||||
teams: teams,
|
teams: teams,
|
||||||
raids: raids,
|
raidGroups: raidGroups,
|
||||||
sortedRaids: sortedRaids,
|
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import api from '~utils/api'
|
||||||
import { setHeaders } from '~utils/userToken'
|
import { setHeaders } from '~utils/userToken'
|
||||||
import extractFilters from '~utils/extractFilters'
|
import extractFilters from '~utils/extractFilters'
|
||||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||||
import organizeRaids from '~utils/organizeRaids'
|
|
||||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
import { defaultFilterset } from '~utils/defaultFilters'
|
import { defaultFilterset } from '~utils/defaultFilters'
|
||||||
|
|
@ -113,6 +112,7 @@ const TeamsRoute: React.FC<Props> = ({
|
||||||
setTotalPages(context.pagination.totalPages)
|
setTotalPages(context.pagination.totalPages)
|
||||||
setRecordCount(context.pagination.count)
|
setRecordCount(context.pagination.count)
|
||||||
replaceResults(context.pagination.count, context.teams)
|
replaceResults(context.pagination.count, context.teams)
|
||||||
|
appState.raidGroups = context.raidGroups
|
||||||
appState.version = version
|
appState.version = version
|
||||||
}
|
}
|
||||||
setCurrentPage(1)
|
setCurrentPage(1)
|
||||||
|
|
@ -388,12 +388,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch and organize raids
|
// Fetch and organize raids
|
||||||
let { raids, sortedRaids } = await api.endpoints.raids
|
let raidGroups: RaidGroup[] = await api
|
||||||
.getAll()
|
.raidGroups()
|
||||||
.then((response) => organizeRaids(response.data))
|
.then((response) => response.data)
|
||||||
|
|
||||||
// Create filter object
|
// Create filter object
|
||||||
const filters: FilterObject = extractFilters(query, raids)
|
const filters: FilterObject = extractFilters(query, raidGroups)
|
||||||
const params = {
|
const params = {
|
||||||
params: { ...filters, ...advancedFilters },
|
params: { ...filters, ...advancedFilters },
|
||||||
}
|
}
|
||||||
|
|
@ -414,8 +414,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
// Consolidate data into context object
|
// Consolidate data into context object
|
||||||
const context: PageContextObj = {
|
const context: PageContextObj = {
|
||||||
teams: teams,
|
teams: teams,
|
||||||
raids: raids,
|
raidGroups: raidGroups,
|
||||||
sortedRaids: sortedRaids,
|
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.93618 4.62991C2.73179 4.44423 2.41557 4.4594 2.22989 4.6638C2.04421 4.8682 2.05938 5.18441 2.26378 5.37009L6.65743 9.36145C6.72962 9.42702 6.81576 9.46755 6.90516 9.48353C7.05808 9.51688 7.2243 9.47812 7.34882 9.3647L11.7346 5.36964C11.9388 5.18368 11.9535 4.86745 11.7676 4.6633C11.5816 4.45916 11.2654 4.44441 11.0612 4.63037L7.00447 8.32569L2.93618 4.62991Z" />
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.84732 2.05019L7.84732 9.92919L10.8934 6.88308C11.2005 6.57597 11.6985 6.57597 12.0056 6.88308C12.3127 7.19019 12.3127 7.68812 12.0056 7.99523L7.56857 12.4322C7.5648 12.4362 7.56098 12.4401 7.55711 12.4439C7.25 12.751 6.75208 12.751 6.44497 12.4439L6.44485 12.4438L1.99638 7.99534C1.68927 7.68823 1.68927 7.19031 1.99638 6.8832C2.30349 6.57609 2.80142 6.57609 3.10853 6.8832L6.27451 10.0492L6.27451 2.05019C6.27451 1.61587 6.6266 1.26379 7.06092 1.26379C7.49524 1.26379 7.84732 1.61587 7.84732 2.05019Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 625 B |
3
public/icons/Chevron.svg
Normal file
3
public/icons/Chevron.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.93618 4.62991C2.73179 4.44423 2.41557 4.4594 2.22989 4.6638C2.04421 4.8682 2.05938 5.18441 2.26378 5.37009L6.65743 9.36145C6.72962 9.42702 6.81576 9.46755 6.90516 9.48353C7.05808 9.51688 7.2243 9.47812 7.34882 9.3647L11.7346 5.36964C11.9388 5.18368 11.9535 4.86745 11.7676 4.6633C11.5816 4.45916 11.2654 4.44441 11.0612 4.63037L7.00447 8.32569L2.93618 4.62991Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 508 B |
|
Before Width: | Height: | Size: 531 B After Width: | Height: | Size: 531 B |
|
|
@ -46,6 +46,9 @@
|
||||||
"new": "New",
|
"new": "New",
|
||||||
"wiki": "View more on gbf.wiki"
|
"wiki": "View more on gbf.wiki"
|
||||||
},
|
},
|
||||||
|
"combobox": {
|
||||||
|
"selected": "Selected"
|
||||||
|
},
|
||||||
"context": {
|
"context": {
|
||||||
"modify": {
|
"modify": {
|
||||||
"character": "Modify character",
|
"character": "Modify character",
|
||||||
|
|
@ -54,25 +57,6 @@
|
||||||
},
|
},
|
||||||
"remove": "Remove from grid"
|
"remove": "Remove from grid"
|
||||||
},
|
},
|
||||||
"filters": {
|
|
||||||
"labels": {
|
|
||||||
"element": "Element",
|
|
||||||
"series": "Series",
|
|
||||||
"proficiency": "Proficiency",
|
|
||||||
"rarity": "Rarity"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"anonymous": "Anonymous",
|
|
||||||
"untitled_team": "Untitled team by {{username}}",
|
|
||||||
"new_team": "New team",
|
|
||||||
"byline": "{{partyName}} by {{username}}"
|
|
||||||
},
|
|
||||||
"rarities": {
|
|
||||||
"r": "R",
|
|
||||||
"sr": "SR",
|
|
||||||
"ssr": "SSR"
|
|
||||||
},
|
|
||||||
"elements": {
|
"elements": {
|
||||||
"null": "Null",
|
"null": "Null",
|
||||||
"wind": "Wind",
|
"wind": "Wind",
|
||||||
|
|
@ -107,64 +91,47 @@
|
||||||
},
|
},
|
||||||
"unauthorized": "You don't have permission to perform that action"
|
"unauthorized": "You don't have permission to perform that action"
|
||||||
},
|
},
|
||||||
"proficiencies": {
|
"filters": {
|
||||||
"sabre": "Sabre",
|
"labels": {
|
||||||
"dagger": "Dagger",
|
"element": "Element",
|
||||||
"spear": "Spear",
|
"series": "Series",
|
||||||
"axe": "Axe",
|
"proficiency": "Proficiency",
|
||||||
"staff": "Staff",
|
"rarity": "Rarity"
|
||||||
"gun": "Gun",
|
}
|
||||||
"melee": "Melee",
|
|
||||||
"bow": "Bow",
|
|
||||||
"harp": "Harp",
|
|
||||||
"katana": "Katana"
|
|
||||||
},
|
},
|
||||||
"series": {
|
"header": {
|
||||||
"seraphic": "Seraphic",
|
"anonymous": "Anonymous",
|
||||||
"grand": "Grand",
|
"untitled_team": "Untitled team by {{username}}",
|
||||||
"opus": "Dark Opus",
|
"new_team": "New team",
|
||||||
"draconic": "Draconic",
|
"byline": "{{partyName}} by {{username}}"
|
||||||
"primal": "Primal",
|
|
||||||
"olden_primal": "Olden Primal",
|
|
||||||
"beast": "Beast",
|
|
||||||
"omega": "Omega",
|
|
||||||
"regalia": "Regalia",
|
|
||||||
"militis": "Militis",
|
|
||||||
"xeno": "Xeno",
|
|
||||||
"astral": "Astral",
|
|
||||||
"rose": "Rose",
|
|
||||||
"hollowsky": "Hollowsky",
|
|
||||||
"ultima": "Ultima",
|
|
||||||
"bahamut": "Bahamut",
|
|
||||||
"epic": "Epic",
|
|
||||||
"ennead": "Ennead",
|
|
||||||
"cosmic": "Cosmic",
|
|
||||||
"ancestral": "Ancestral",
|
|
||||||
"superlative": "Superlative",
|
|
||||||
"vintage": "Vintage",
|
|
||||||
"class_champion": "Class Champion",
|
|
||||||
"sephira": "Sephira",
|
|
||||||
"new_world": "New World Foundation",
|
|
||||||
"revenant": "Revenant",
|
|
||||||
"proving": "Proven",
|
|
||||||
"disaster": "Revans",
|
|
||||||
"illustrious": "Illustrious",
|
|
||||||
"world": "World"
|
|
||||||
},
|
},
|
||||||
"recency": {
|
"job_skills": {
|
||||||
"all_time": "All time",
|
"all": "All skills",
|
||||||
"last_day": "Last day",
|
"buffing": "Buffing",
|
||||||
"last_week": "Last week",
|
"debuffing": "Debuffing",
|
||||||
"last_month": "Last month",
|
"damaging": "Damaging",
|
||||||
"last_3_months": "Last 3 months",
|
"healing": "Healing",
|
||||||
"last_6_months": "Last 6 months",
|
"emp": "Extended Mastery",
|
||||||
"last_year": "Last year"
|
"base": "Base Skills",
|
||||||
|
"state": {
|
||||||
|
"selectable": "Select a skill",
|
||||||
|
"no_skill": "No skill"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"summons": {
|
"menu": {
|
||||||
"main": "Main Summon",
|
"about": "About",
|
||||||
"friend": "Friend Summon",
|
"changelog": "Changelog",
|
||||||
"summons": "Summons",
|
"guides": "Guides",
|
||||||
"subaura": "Sub Aura Summons"
|
"language": "Language",
|
||||||
|
"login": "Log in",
|
||||||
|
"roadmap": "Roadmap",
|
||||||
|
"profile": "Your profile",
|
||||||
|
"new": "New party",
|
||||||
|
"saved": "Saved",
|
||||||
|
"settings": "Settings",
|
||||||
|
"signup": "Sign up",
|
||||||
|
"teams": "Teams",
|
||||||
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"about": {
|
"about": {
|
||||||
|
|
@ -341,20 +308,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"page": {
|
||||||
"about": "About",
|
"titles": {
|
||||||
"changelog": "Changelog",
|
"about": "About granblue.team",
|
||||||
"guides": "Guides",
|
"updates": "Updates / granblue.team",
|
||||||
"language": "Language",
|
"roadmap": "Roadmap / granblue.team",
|
||||||
"login": "Log in",
|
"discover": "Discover teams / granblue.team",
|
||||||
"roadmap": "Roadmap",
|
"new": "Create a new team / granblue.team",
|
||||||
"profile": "Your profile",
|
"profile": "@{{username}}'s Teams / granblue.team",
|
||||||
"new": "New party",
|
"team": "{{emoji}} {{teamName}} by {{username}} / granblue.team",
|
||||||
"saved": "Saved",
|
"saved": "Your saved teams / granblue.team"
|
||||||
"settings": "Settings",
|
},
|
||||||
"signup": "Sign up",
|
"descriptions": {
|
||||||
"teams": "Teams",
|
"about": "More about granblue.team / Save and discover teams to use in Granblue Fantasy",
|
||||||
"logout": "Logout"
|
"updates": "Latest updates to granblue.team",
|
||||||
|
"roadmap": "Upcoming planned features for granblue.team",
|
||||||
|
"discover": "Save and discover teams to use in Granblue Fantasy and search by raid, element or recency",
|
||||||
|
"new": "Create and theorycraft teams to use in Granblue Fantasy and share with the community",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"party": {
|
"party": {
|
||||||
"segmented_control": {
|
"segmented_control": {
|
||||||
|
|
@ -384,6 +357,40 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proficiencies": {
|
||||||
|
"sabre": "Sabre",
|
||||||
|
"dagger": "Dagger",
|
||||||
|
"spear": "Spear",
|
||||||
|
"axe": "Axe",
|
||||||
|
"staff": "Staff",
|
||||||
|
"gun": "Gun",
|
||||||
|
"melee": "Melee",
|
||||||
|
"bow": "Bow",
|
||||||
|
"harp": "Harp",
|
||||||
|
"katana": "Katana"
|
||||||
|
},
|
||||||
|
"raids": {
|
||||||
|
"placeholder": "Select a raid...",
|
||||||
|
"sections": {
|
||||||
|
"raids": "Raids",
|
||||||
|
"solo": "Solo",
|
||||||
|
"events": "Events"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rarities": {
|
||||||
|
"r": "R",
|
||||||
|
"sr": "SR",
|
||||||
|
"ssr": "SSR"
|
||||||
|
},
|
||||||
|
"recency": {
|
||||||
|
"all_time": "All time",
|
||||||
|
"last_day": "Last day",
|
||||||
|
"last_week": "Last week",
|
||||||
|
"last_month": "Last month",
|
||||||
|
"last_3_months": "Last 3 months",
|
||||||
|
"last_6_months": "Last 6 months",
|
||||||
|
"last_year": "Last year"
|
||||||
|
},
|
||||||
"saved": {
|
"saved": {
|
||||||
"title": "Your saved Teams",
|
"title": "Your saved Teams",
|
||||||
"loading": "Loading saved teams...",
|
"loading": "Loading saved teams...",
|
||||||
|
|
@ -403,48 +410,53 @@
|
||||||
"summon": "Search for a summon...",
|
"summon": "Search for a summon...",
|
||||||
"character": "Search for a character...",
|
"character": "Search for a character...",
|
||||||
"job_skill": "Search job skills...",
|
"job_skill": "Search job skills...",
|
||||||
"guidebook": "Search guidebooks..."
|
"guidebook": "Search guidebooks...",
|
||||||
|
"raid": "Search battles..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"series": {
|
||||||
|
"seraphic": "Seraphic",
|
||||||
|
"grand": "Grand",
|
||||||
|
"opus": "Dark Opus",
|
||||||
|
"draconic": "Draconic",
|
||||||
|
"primal": "Primal",
|
||||||
|
"olden_primal": "Olden Primal",
|
||||||
|
"beast": "Beast",
|
||||||
|
"omega": "Omega",
|
||||||
|
"regalia": "Regalia",
|
||||||
|
"militis": "Militis",
|
||||||
|
"xeno": "Xeno",
|
||||||
|
"astral": "Astral",
|
||||||
|
"rose": "Rose",
|
||||||
|
"hollowsky": "Hollowsky",
|
||||||
|
"ultima": "Ultima",
|
||||||
|
"bahamut": "Bahamut",
|
||||||
|
"epic": "Epic",
|
||||||
|
"ennead": "Ennead",
|
||||||
|
"cosmic": "Cosmic",
|
||||||
|
"ancestral": "Ancestral",
|
||||||
|
"superlative": "Superlative",
|
||||||
|
"vintage": "Vintage",
|
||||||
|
"class_champion": "Class Champion",
|
||||||
|
"sephira": "Sephira",
|
||||||
|
"new_world": "New World Foundation",
|
||||||
|
"revenant": "Revenant",
|
||||||
|
"proving": "Proven",
|
||||||
|
"disaster": "Revans",
|
||||||
|
"illustrious": "Illustrious",
|
||||||
|
"world": "World"
|
||||||
|
},
|
||||||
|
"summons": {
|
||||||
|
"main": "Main Summon",
|
||||||
|
"friend": "Friend Summon",
|
||||||
|
"summons": "Summons",
|
||||||
|
"subaura": "Sub Aura Summons"
|
||||||
|
},
|
||||||
"teams": {
|
"teams": {
|
||||||
"title": "Discover Teams",
|
"title": "Discover Teams",
|
||||||
"loading": "Loading teams...",
|
"loading": "Loading teams...",
|
||||||
"not_found": "No teams found"
|
"not_found": "No teams found"
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"titles": {
|
|
||||||
"about": "About granblue.team",
|
|
||||||
"updates": "Updates / granblue.team",
|
|
||||||
"roadmap": "Roadmap / granblue.team",
|
|
||||||
"discover": "Discover teams / granblue.team",
|
|
||||||
"new": "Create a new team / granblue.team",
|
|
||||||
"profile": "@{{username}}'s Teams / granblue.team",
|
|
||||||
"team": "{{emoji}} {{teamName}} by {{username}} / granblue.team",
|
|
||||||
"saved": "Your saved teams / granblue.team"
|
|
||||||
},
|
|
||||||
"descriptions": {
|
|
||||||
"about": "More about granblue.team / Save and discover teams to use in Granblue Fantasy",
|
|
||||||
"updates": "Latest updates to granblue.team",
|
|
||||||
"roadmap": "Upcoming planned features for granblue.team",
|
|
||||||
"discover": "Save and discover teams to use in Granblue Fantasy and search by raid, element or recency",
|
|
||||||
"new": "Create and theorycraft teams to use in Granblue Fantasy and share with the community",
|
|
||||||
"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",
|
|
||||||
"debuffing": "Debuffing",
|
|
||||||
"damaging": "Damaging",
|
|
||||||
"healing": "Healing",
|
|
||||||
"emp": "Extended Mastery",
|
|
||||||
"base": "Base Skills",
|
|
||||||
"state": {
|
|
||||||
"selectable": "Select a skill",
|
|
||||||
"no_skill": "No skill"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"copied": "This party's URL was copied to your clipboard",
|
"copied": "This party's URL was copied to your clipboard",
|
||||||
"remixed": "You remixed {{title}}",
|
"remixed": "You remixed {{title}}",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@
|
||||||
"new": "作成",
|
"new": "作成",
|
||||||
"wiki": "gbf.wikiで詳しく見る"
|
"wiki": "gbf.wikiで詳しく見る"
|
||||||
},
|
},
|
||||||
|
"combobox": {
|
||||||
|
"selected": "選択済み"
|
||||||
|
},
|
||||||
"context": {
|
"context": {
|
||||||
"modify": {
|
"modify": {
|
||||||
"character": "キャラクターを変更",
|
"character": "キャラクターを変更",
|
||||||
|
|
@ -54,40 +57,6 @@
|
||||||
},
|
},
|
||||||
"remove": "編成から削除"
|
"remove": "編成から削除"
|
||||||
},
|
},
|
||||||
"filters": {
|
|
||||||
"labels": {
|
|
||||||
"element": "属性",
|
|
||||||
"series": "シリーズ",
|
|
||||||
"proficiency": "武器種",
|
|
||||||
"rarity": "レアリティ"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"internal_server_error": {
|
|
||||||
"title": "サーバーエラー",
|
|
||||||
"description": "サーバーから届いたエラーは自動的に復されなかったため、再びリクエストを行なってください"
|
|
||||||
},
|
|
||||||
"not_found": {
|
|
||||||
"title": "見つかりませんでした",
|
|
||||||
"description": "探しているページは見つかりませんでした",
|
|
||||||
"button": "新しい編成を作成"
|
|
||||||
},
|
|
||||||
"validation": {
|
|
||||||
"guidebooks": "セフィラ導本を複数個装備することはできません"
|
|
||||||
},
|
|
||||||
"unauthorized": "行ったアクションを実行する権限がありません"
|
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"anonymous": "無名",
|
|
||||||
"untitled_team": "{{username}}さんからの無題編成",
|
|
||||||
"new_team": "新編成",
|
|
||||||
"byline": "{{username}}さんからの{{partyName}}"
|
|
||||||
},
|
|
||||||
"rarities": {
|
|
||||||
"r": "R",
|
|
||||||
"sr": "SR",
|
|
||||||
"ssr": "SSR"
|
|
||||||
},
|
|
||||||
"elements": {
|
"elements": {
|
||||||
"null": "無",
|
"null": "無",
|
||||||
"wind": "風",
|
"wind": "風",
|
||||||
|
|
@ -107,64 +76,62 @@
|
||||||
"light": "光属性"
|
"light": "光属性"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proficiencies": {
|
"errors": {
|
||||||
"sabre": "剣",
|
"internal_server_error": {
|
||||||
"dagger": "短剣",
|
"title": "サーバーエラー",
|
||||||
"spear": "槍",
|
"description": "サーバーから届いたエラーは自動的に復されなかったため、再びリクエストを行なってください"
|
||||||
"axe": "斧",
|
},
|
||||||
"staff": "杖",
|
"not_found": {
|
||||||
"gun": "銃",
|
"title": "見つかりませんでした",
|
||||||
"melee": "拳",
|
"description": "探しているページは見つかりませんでした",
|
||||||
"bow": "弓",
|
"button": "新しい編成を作成"
|
||||||
"harp": "琴",
|
},
|
||||||
"katana": "刀"
|
"validation": {
|
||||||
|
"guidebooks": "セフィラ導本を複数個装備することはできません"
|
||||||
|
},
|
||||||
|
"unauthorized": "行ったアクションを実行する権限がありません"
|
||||||
},
|
},
|
||||||
"series": {
|
"filters": {
|
||||||
"seraphic": "セラフィックウェポン",
|
"labels": {
|
||||||
"grand": "リミテッドシリーズ",
|
"element": "属性",
|
||||||
"opus": "終末の神器",
|
"series": "シリーズ",
|
||||||
"draconic": "ドラコニックウェポン",
|
"proficiency": "武器種",
|
||||||
"primal": "プライマルシリーズ",
|
"rarity": "レアリティ"
|
||||||
"olden_primal": "オールド・プライマルシリーズ",
|
}
|
||||||
"beast": "四象武器",
|
|
||||||
"omega": "マグナシリーズ",
|
|
||||||
"regalia": "レガリアシリーズ",
|
|
||||||
"militis": "ミーレスシリーズ",
|
|
||||||
"xeno": "六道武器",
|
|
||||||
"astral": "アストラルウェポン",
|
|
||||||
"rose": "ローズシリーズ",
|
|
||||||
"hollowsky": "虚ろなる神器",
|
|
||||||
"ultima": "オメガウェポン",
|
|
||||||
"bahamut": "バハムートウェポン",
|
|
||||||
"epic": "エピックウェポン",
|
|
||||||
"ennead": "エニアドシリーズ",
|
|
||||||
"cosmic": "コスモスシリーズ",
|
|
||||||
"ancestral": "アンセスタルシリーズ",
|
|
||||||
"superlative": "スペリオシリーズ",
|
|
||||||
"vintage": "ヴィンテージシリーズ",
|
|
||||||
"class_champion": "英雄武器",
|
|
||||||
"sephira": "セフィラン・オールドウェポン",
|
|
||||||
"new_world": "新世界の礎",
|
|
||||||
"revenant": "天星器",
|
|
||||||
"proving": "ブレイブウェポン",
|
|
||||||
"disaster": "レヴァンスウェポン",
|
|
||||||
"illustrious": "ルミナスシリーズ",
|
|
||||||
"world": "ワールドシリーズ"
|
|
||||||
},
|
},
|
||||||
"recency": {
|
"header": {
|
||||||
"all_time": "全ての期間",
|
"anonymous": "無名",
|
||||||
"last_day": "1日",
|
"untitled_team": "{{username}}さんからの無題編成",
|
||||||
"last_week": "7日",
|
"new_team": "新編成",
|
||||||
"last_month": "1ヶ月",
|
"byline": "{{username}}さんからの{{partyName}}"
|
||||||
"last_3_months": "3ヶ月",
|
|
||||||
"last_6_months": "6ヶ月",
|
|
||||||
"last_year": "1年"
|
|
||||||
},
|
},
|
||||||
"summons": {
|
"job_skills": {
|
||||||
"main": "メイン",
|
"all": "全てのアビリティ",
|
||||||
"friend": "フレンド",
|
"buffing": "強化アビリティ",
|
||||||
"summons": "召喚石",
|
"debuffing": "弱体アビリティ",
|
||||||
"subaura": "サブ加護召喚石"
|
"damaging": "ダメージアビリティ",
|
||||||
|
"healing": "回復アビリティ",
|
||||||
|
"emp": "リミットアビリティ",
|
||||||
|
"base": "ベースアビリティ",
|
||||||
|
"state": {
|
||||||
|
"selectable": "アビリティを選択",
|
||||||
|
"no_skill": "設定されていません"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"about": "このサイトについて",
|
||||||
|
"changelog": "変更ログ",
|
||||||
|
"guides": "攻略",
|
||||||
|
"language": "言語",
|
||||||
|
"login": "ログイン",
|
||||||
|
"profile": "プロフィール",
|
||||||
|
"roadmap": "ロードマップ",
|
||||||
|
"new": "新しい編成",
|
||||||
|
"saved": "保存した編成",
|
||||||
|
"settings": "アカウント設定",
|
||||||
|
"signup": "登録",
|
||||||
|
"teams": "編成一覧",
|
||||||
|
"logout": "ログアウト"
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"about": {
|
"about": {
|
||||||
|
|
@ -342,20 +309,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"page": {
|
||||||
"about": "このサイトについて",
|
"titles": {
|
||||||
"changelog": "変更ログ",
|
"about": "granblue.teamについて",
|
||||||
"guides": "攻略",
|
"updates": "変更ログ / granblue.team",
|
||||||
"language": "言語",
|
"roadmap": "ロードマップ / granblue.team",
|
||||||
"login": "ログイン",
|
"discover": "編成を見出す / granblue.team",
|
||||||
"profile": "プロフィール",
|
"new": "新しい編成 / granblue.team",
|
||||||
"roadmap": "ロードマップ",
|
"profile": "@{{username}}さんの作った編成 / granblue.team",
|
||||||
"new": "新しい編成",
|
"team": "{{emoji}} {{teamName}}、{{username}}さんから / granblue.team",
|
||||||
"saved": "保存した編成",
|
"saved": "保存した編成"
|
||||||
"settings": "アカウント設定",
|
},
|
||||||
"signup": "登録",
|
"descriptions": {
|
||||||
"teams": "編成一覧",
|
"about": "granblue.teamについて / グランブルーファンタジーの編成を探したり保存したりできる",
|
||||||
"logout": "ログアウト"
|
"updates": "granblue.teamの最新変更について",
|
||||||
|
"roadmap": "granblue.teamの開発予定機能",
|
||||||
|
"discover": "グランブルーファンタジーの編成をマルチ、属性、作った時間などで探したり保存したりできる",
|
||||||
|
"new": "グランブルーファンタジーの編成を作成し、騎空士とシェアできるサイトgranblue.team",
|
||||||
|
"profile": "@{{username}}の編成を調査し、マルチ、属性、または作った時間でフィルターする",
|
||||||
|
"team": "granblue.teamで{{username}}さんが作った{{raidName}}の編成を調査できる"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"party": {
|
"party": {
|
||||||
"segmented_control": {
|
"segmented_control": {
|
||||||
|
|
@ -385,6 +358,40 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proficiencies": {
|
||||||
|
"sabre": "剣",
|
||||||
|
"dagger": "短剣",
|
||||||
|
"spear": "槍",
|
||||||
|
"axe": "斧",
|
||||||
|
"staff": "杖",
|
||||||
|
"gun": "銃",
|
||||||
|
"melee": "拳",
|
||||||
|
"bow": "弓",
|
||||||
|
"harp": "琴",
|
||||||
|
"katana": "刀"
|
||||||
|
},
|
||||||
|
"raids": {
|
||||||
|
"placeholder": "バトルを選択...",
|
||||||
|
"sections": {
|
||||||
|
"raids": "マルチ",
|
||||||
|
"solo": "ソロ",
|
||||||
|
"events": "イベント"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rarities": {
|
||||||
|
"r": "R",
|
||||||
|
"sr": "SR",
|
||||||
|
"ssr": "SSR"
|
||||||
|
},
|
||||||
|
"recency": {
|
||||||
|
"all_time": "全ての期間",
|
||||||
|
"last_day": "1日",
|
||||||
|
"last_week": "7日",
|
||||||
|
"last_month": "1ヶ月",
|
||||||
|
"last_3_months": "3ヶ月",
|
||||||
|
"last_6_months": "6ヶ月",
|
||||||
|
"last_year": "1年"
|
||||||
|
},
|
||||||
"saved": {
|
"saved": {
|
||||||
"title": "保存した編成",
|
"title": "保存した編成",
|
||||||
"loading": "ロード中...",
|
"loading": "ロード中...",
|
||||||
|
|
@ -404,48 +411,53 @@
|
||||||
"summon": "召喚石を検索...",
|
"summon": "召喚石を検索...",
|
||||||
"character": "キャラを検索...",
|
"character": "キャラを検索...",
|
||||||
"job_skill": "ジョブのスキルを検索...",
|
"job_skill": "ジョブのスキルを検索...",
|
||||||
"guidebook": "導本を検索..."
|
"guidebook": "導本を検索...",
|
||||||
|
"raid": "バトルを検索..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"series": {
|
||||||
|
"seraphic": "セラフィックウェポン",
|
||||||
|
"grand": "リミテッドシリーズ",
|
||||||
|
"opus": "終末の神器",
|
||||||
|
"draconic": "ドラコニックウェポン",
|
||||||
|
"primal": "プライマルシリーズ",
|
||||||
|
"olden_primal": "オールド・プライマルシリーズ",
|
||||||
|
"beast": "四象武器",
|
||||||
|
"omega": "マグナシリーズ",
|
||||||
|
"regalia": "レガリアシリーズ",
|
||||||
|
"militis": "ミーレスシリーズ",
|
||||||
|
"xeno": "六道武器",
|
||||||
|
"astral": "アストラルウェポン",
|
||||||
|
"rose": "ローズシリーズ",
|
||||||
|
"hollowsky": "虚ろなる神器",
|
||||||
|
"ultima": "オメガウェポン",
|
||||||
|
"bahamut": "バハムートウェポン",
|
||||||
|
"epic": "エピックウェポン",
|
||||||
|
"ennead": "エニアドシリーズ",
|
||||||
|
"cosmic": "コスモスシリーズ",
|
||||||
|
"ancestral": "アンセスタルシリーズ",
|
||||||
|
"superlative": "スペリオシリーズ",
|
||||||
|
"vintage": "ヴィンテージシリーズ",
|
||||||
|
"class_champion": "英雄武器",
|
||||||
|
"sephira": "セフィラン・オールドウェポン",
|
||||||
|
"new_world": "新世界の礎",
|
||||||
|
"revenant": "天星器",
|
||||||
|
"proving": "ブレイブウェポン",
|
||||||
|
"disaster": "レヴァンスウェポン",
|
||||||
|
"illustrious": "ルミナスシリーズ",
|
||||||
|
"world": "ワールドシリーズ"
|
||||||
|
},
|
||||||
|
"summons": {
|
||||||
|
"main": "メイン",
|
||||||
|
"friend": "フレンド",
|
||||||
|
"summons": "召喚石",
|
||||||
|
"subaura": "サブ加護召喚石"
|
||||||
|
},
|
||||||
"teams": {
|
"teams": {
|
||||||
"title": "編成一覧",
|
"title": "編成一覧",
|
||||||
"loading": "ロード中...",
|
"loading": "ロード中...",
|
||||||
"not_found": "編成は見つかりませんでした"
|
"not_found": "編成は見つかりませんでした"
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"titles": {
|
|
||||||
"about": "granblue.teamについて",
|
|
||||||
"updates": "変更ログ / granblue.team",
|
|
||||||
"roadmap": "ロードマップ / granblue.team",
|
|
||||||
"discover": "編成を見出す / granblue.team",
|
|
||||||
"new": "新しい編成 / granblue.team",
|
|
||||||
"profile": "@{{username}}さんの作った編成 / granblue.team",
|
|
||||||
"team": "{{emoji}} {{teamName}}、{{username}}さんから / granblue.team",
|
|
||||||
"saved": "保存した編成"
|
|
||||||
},
|
|
||||||
"descriptions": {
|
|
||||||
"about": "granblue.teamについて / グランブルーファンタジーの編成を探したり保存したりできる",
|
|
||||||
"updates": "granblue.teamの最新変更について",
|
|
||||||
"roadmap": "granblue.teamの開発予定機能",
|
|
||||||
"discover": "グランブルーファンタジーの編成をマルチ、属性、作った時間などで探したり保存したりできる",
|
|
||||||
"new": "グランブルーファンタジーの編成を作成し、騎空士とシェアできるサイトgranblue.team",
|
|
||||||
"profile": "@{{username}}の編成を調査し、マルチ、属性、または作った時間でフィルターする",
|
|
||||||
"team": "granblue.teamで{{username}}さんが作った{{raidName}}の編成を調査できる"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"job_skills": {
|
|
||||||
"all": "全てのアビリティ",
|
|
||||||
"buffing": "強化アビリティ",
|
|
||||||
"debuffing": "弱体アビリティ",
|
|
||||||
"damaging": "ダメージアビリティ",
|
|
||||||
"healing": "回復アビリティ",
|
|
||||||
"emp": "リミットアビリティ",
|
|
||||||
"base": "ベースアビリティ",
|
|
||||||
"state": {
|
|
||||||
"selectable": "アビリティを選択",
|
|
||||||
"no_skill": "設定されていません"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"copied": "この編成のURLはクリップボードにコピーされました",
|
"copied": "この編成のURLはクリップボードにコピーされました",
|
||||||
"remixed": "{{title}}をリミックスしました",
|
"remixed": "{{title}}をリミックスしました",
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,17 @@ a {
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
|
border: 2px solid transparent;
|
||||||
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial,
|
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
font-size: $font-regular;
|
font-size: $font-regular;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:focus-visible {
|
||||||
|
border: 2px solid $blue;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
|
@ -300,3 +306,50 @@ i.tag {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Joined {
|
||||||
|
$offset: 2px;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border-radius: $input-corner;
|
||||||
|
border: $offset solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
gap: $unit;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
padding-right: calc($unit-2x - $offset);
|
||||||
|
|
||||||
|
&.Bound {
|
||||||
|
background-color: var(--input-bound-bg);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--input-bound-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border: $offset solid $blue;
|
||||||
|
// box-shadow: 0 2px rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Counter {
|
||||||
|
color: $grey-55;
|
||||||
|
font-weight: $bold;
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: $unit * 1.5 $unit-2x;
|
||||||
|
padding-left: calc($unit-2x - $offset);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,12 @@
|
||||||
--subaura-orange-secondary: #{$subaura--orange--secondary--light};
|
--subaura-orange-secondary: #{$subaura--orange--secondary--light};
|
||||||
--subaura-orange-text: #{$subaura--orange--text--light};
|
--subaura-orange-text: #{$subaura--orange--text--light};
|
||||||
|
|
||||||
|
// Light - Pills
|
||||||
|
--pill-bg: #{$pill--bg--light};
|
||||||
|
--pill-bg-hover: #{$pill--bg--light--hover};
|
||||||
|
--pill-text: #{$pill--text--light};
|
||||||
|
--pill-text-hover: #{$pill--text--light--hover};
|
||||||
|
|
||||||
// Light - Element Toggle
|
// Light - Element Toggle
|
||||||
--toggle-bg: #{$toggle--bg--light};
|
--toggle-bg: #{$toggle--bg--light};
|
||||||
--toggle-stroke: #{$toggle--stroke--light};
|
--toggle-stroke: #{$toggle--stroke--light};
|
||||||
|
|
@ -259,6 +265,12 @@
|
||||||
--subaura-orange-secondary: #{$subaura--orange--secondary--dark};
|
--subaura-orange-secondary: #{$subaura--orange--secondary--dark};
|
||||||
--subaura-orange-text: #{$subaura--orange--text--dark};
|
--subaura-orange-text: #{$subaura--orange--text--dark};
|
||||||
|
|
||||||
|
// Dark - Pills
|
||||||
|
--pill-bg: #{$pill--bg--dark};
|
||||||
|
--pill-bg-hover: #{$pill--bg--dark--hover};
|
||||||
|
--pill-text: #{$pill--text--dark};
|
||||||
|
--pill-text-hover: #{$pill--text--dark--hover};
|
||||||
|
|
||||||
// Dark - Element Toggle
|
// Dark - Element Toggle
|
||||||
--toggle-bg: #{$toggle--bg--dark};
|
--toggle-bg: #{$toggle--bg--dark};
|
||||||
--toggle-stroke: #{$toggle--stroke--dark};
|
--toggle-stroke: #{$toggle--stroke--dark};
|
||||||
|
|
@ -268,32 +280,32 @@
|
||||||
// Element theming
|
// Element theming
|
||||||
--wind-bg: #{$wind-bg-10};
|
--wind-bg: #{$wind-bg-10};
|
||||||
--wind-hover-bg: #{$wind-bg-00};
|
--wind-hover-bg: #{$wind-bg-00};
|
||||||
--wind-text: #{$wind-text-10};
|
--wind-text: #{$wind-text-20};
|
||||||
--wind-hover-text: #{$wind-text-00};
|
--wind-hover-text: #{$wind-text-00};
|
||||||
|
|
||||||
--fire-bg: #{$fire-bg-10};
|
--fire-bg: #{$fire-bg-10};
|
||||||
--fire-hover-bg: #{$fire-bg-00};
|
--fire-hover-bg: #{$fire-bg-00};
|
||||||
--fire-text: #{$fire-text-10};
|
--fire-text: #{$fire-text-20};
|
||||||
--fire-hover-text: #{$fire-text-00};
|
--fire-hover-text: #{$fire-text-00};
|
||||||
|
|
||||||
--water-bg: #{$water-bg-10};
|
--water-bg: #{$water-bg-10};
|
||||||
--water-hover-bg: #{$water-bg-00};
|
--water-hover-bg: #{$water-bg-00};
|
||||||
--water-text: #{$water-text-10};
|
--water-text: #{$water-text-20};
|
||||||
--water-hover-text: #{$water-text-00};
|
--water-hover-text: #{$water-text-00};
|
||||||
|
|
||||||
--earth-bg: #{$earth-bg-10};
|
--earth-bg: #{$earth-bg-10};
|
||||||
--earth-hover-bg: #{$earth-bg-00};
|
--earth-hover-bg: #{$earth-bg-00};
|
||||||
--earth-text: #{$earth-text-10};
|
--earth-text: #{$earth-text-20};
|
||||||
--earth-hover-text: #{$earth-text-00};
|
--earth-hover-text: #{$earth-text-00};
|
||||||
|
|
||||||
--dark-bg: #{$dark-bg-10};
|
--dark-bg: #{$dark-bg-10};
|
||||||
--dark-hover-bg: #{$dark-bg-00};
|
--dark-hover-bg: #{$dark-bg-00};
|
||||||
--dark-text: #{$dark-text-10};
|
--dark-text: #{$dark-text-20};
|
||||||
--dark-hover-text: #{$dark-text-00};
|
--dark-hover-text: #{$dark-text-00};
|
||||||
|
|
||||||
--light-bg: #{$light-bg-10};
|
--light-bg: #{$light-bg-10};
|
||||||
--light-hover-bg: #{$light-bg-00};
|
--light-hover-bg: #{$light-bg-00};
|
||||||
--light-text: #{$light-text-10};
|
--light-text: #{$light-text-20};
|
||||||
--light-hover-text: #{$light-text-00};
|
--light-hover-text: #{$light-text-00};
|
||||||
|
|
||||||
// Gradients
|
// Gradients
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ $grey-100: white;
|
||||||
|
|
||||||
// Purple -- Additional Weapons
|
// Purple -- Additional Weapons
|
||||||
$purple-00: #25224e;
|
$purple-00: #25224e;
|
||||||
|
$purple-05: #373278;
|
||||||
$purple-10: #4f3c79;
|
$purple-10: #4f3c79;
|
||||||
$purple-20: #635fb7;
|
$purple-20: #635fb7;
|
||||||
$purple-30: #8c86ff;
|
$purple-30: #8c86ff;
|
||||||
|
|
@ -256,9 +257,10 @@ $extra--purple--card--bg--light: $purple-80;
|
||||||
$extra--purple--primary--light: $purple-30;
|
$extra--purple--primary--light: $purple-30;
|
||||||
$extra--purple--secondary--light: $purple-40;
|
$extra--purple--secondary--light: $purple-40;
|
||||||
$extra--purple--text--light: $purple-10;
|
$extra--purple--text--light: $purple-10;
|
||||||
|
|
||||||
$extra--purple--bg--dark: $purple-20;
|
$extra--purple--bg--dark: $purple-20;
|
||||||
$extra--purple--card--bg--dark: $purple-40;
|
$extra--purple--card--bg--dark: $purple-40;
|
||||||
$extra--purple--primary--dark: $purple-00;
|
$extra--purple--primary--dark: $purple-05;
|
||||||
$extra--purple--secondary--dark: $purple-10;
|
$extra--purple--secondary--dark: $purple-10;
|
||||||
$extra--purple--text--dark: $purple-00;
|
$extra--purple--text--dark: $purple-00;
|
||||||
|
|
||||||
|
|
@ -283,6 +285,17 @@ $full--auto--bg: $yellow-text-20;
|
||||||
$auto--guard--bg: $purple-30;
|
$auto--guard--bg: $purple-30;
|
||||||
$auto--guard--text: $purple-10;
|
$auto--guard--text: $purple-10;
|
||||||
|
|
||||||
|
// Color Definitions: Pills
|
||||||
|
$pill--bg--light: $grey-90;
|
||||||
|
$pill--bg--light--hover: $grey-50;
|
||||||
|
$pill--text--light: $grey-30;
|
||||||
|
$pill--text--light--hover: $grey-100;
|
||||||
|
|
||||||
|
$pill--bg--dark: $grey-00;
|
||||||
|
$pill--bg--dark--hover: $grey-50;
|
||||||
|
$pill--text--dark: $grey-100;
|
||||||
|
$pill--text--dark--hover: $grey-00;
|
||||||
|
|
||||||
// Color Definitions: Element Toggle
|
// Color Definitions: Element Toggle
|
||||||
$toggle--bg--light: $grey-90;
|
$toggle--bg--light: $grey-90;
|
||||||
$toggle--bg--dark: $grey-15;
|
$toggle--bg--dark: $grey-15;
|
||||||
|
|
@ -342,9 +355,10 @@ $scale-wide: scale(1.05, 1.05);
|
||||||
$scale-tall: scale(1.012, 1.012);
|
$scale-tall: scale(1.012, 1.012);
|
||||||
|
|
||||||
// Border radius
|
// Border radius
|
||||||
|
$full-corner: 500px;
|
||||||
$card-corner: $unit * 1.5;
|
$card-corner: $unit * 1.5;
|
||||||
$input-corner: $unit;
|
$input-corner: $unit;
|
||||||
$item-corner: $unit-half;
|
$item-corner: $unit;
|
||||||
|
|
||||||
// Shadows
|
// Shadows
|
||||||
$hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
|
$hover-stroke: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
|
|
||||||
2
types/Raid.d.ts
vendored
2
types/Raid.d.ts
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
interface Raid {
|
interface Raid {
|
||||||
id: string
|
id: string
|
||||||
|
group: RaidGroup
|
||||||
name: {
|
name: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
en: string
|
en: string
|
||||||
|
|
@ -7,6 +8,5 @@ interface Raid {
|
||||||
}
|
}
|
||||||
slug: string
|
slug: string
|
||||||
level: number
|
level: number
|
||||||
group: number
|
|
||||||
element: number
|
element: number
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
types/RaidGroup.d.ts
vendored
Normal file
14
types/RaidGroup.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
interface RaidGroup {
|
||||||
|
id: string
|
||||||
|
name: {
|
||||||
|
[key: string]: string
|
||||||
|
en: string
|
||||||
|
ja: string
|
||||||
|
}
|
||||||
|
raids: Raid[]
|
||||||
|
difficulty: number
|
||||||
|
section: number
|
||||||
|
order: number
|
||||||
|
extra: boolean
|
||||||
|
hl: boolean
|
||||||
|
}
|
||||||
3
types/index.d.ts
vendored
3
types/index.d.ts
vendored
|
|
@ -83,8 +83,7 @@ interface PageContextObj {
|
||||||
party?: Party
|
party?: Party
|
||||||
jobs?: Job[]
|
jobs?: Job[]
|
||||||
jobSkills?: JobSkill[]
|
jobSkills?: JobSkill[]
|
||||||
raids: Raid[]
|
raidGroups: RaidGroup[]
|
||||||
sortedRaids: Raid[][]
|
|
||||||
weaponKeys?: GroupedWeaponKeys
|
weaponKeys?: GroupedWeaponKeys
|
||||||
pagination?: PaginationObject
|
pagination?: PaginationObject
|
||||||
meta?: { [key: string]: string }
|
meta?: { [key: string]: string }
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,11 @@ class Api {
|
||||||
return axios.get(resourceUrl, params)
|
return axios.get(resourceUrl, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
raidGroups(params?: {}) {
|
||||||
|
const resourceUrl = `${this.url}/raids/groups`
|
||||||
|
return axios.get(resourceUrl, params)
|
||||||
|
}
|
||||||
|
|
||||||
remix({ shortcode, body, params}: { shortcode: string, body?: {}, params?: {} }) {
|
remix({ shortcode, body, params}: { shortcode: string, body?: {}, params?: {} }) {
|
||||||
const resourceUrl = `${this.url}/parties/${shortcode}/remix`
|
const resourceUrl = `${this.url}/parties/${shortcode}/remix`
|
||||||
return axios.post(resourceUrl, body, params)
|
return axios.post(resourceUrl, body, params)
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ interface AppState {
|
||||||
summons: Summon[]
|
summons: Summon[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
raids: Raid[]
|
raidGroups: RaidGroup[]
|
||||||
jobs: Job[]
|
jobs: Job[]
|
||||||
jobSkills: JobSkill[]
|
jobSkills: JobSkill[]
|
||||||
weaponKeys: GroupedWeaponKeys
|
weaponKeys: GroupedWeaponKeys
|
||||||
|
|
@ -149,7 +149,7 @@ export const initialAppState: AppState = {
|
||||||
summons: [],
|
summons: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
raids: [],
|
raidGroups: [],
|
||||||
jobs: [],
|
jobs: [],
|
||||||
jobSkills: [],
|
jobSkills: [],
|
||||||
weaponKeys: {
|
weaponKeys: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { elements, allElement } from '~data/elements'
|
import { elements, allElement } from '~data/elements'
|
||||||
|
|
||||||
export default (query: { [index: string]: string }, raids: Raid[]) => {
|
export default (
|
||||||
|
query: { [index: string]: string },
|
||||||
|
raidGroups: RaidGroup[]
|
||||||
|
) => {
|
||||||
// Extract recency filter
|
// Extract recency filter
|
||||||
const recencyParam: number = parseInt(query.recency)
|
const recencyParam: number = parseInt(query.recency)
|
||||||
|
|
||||||
|
|
@ -14,8 +17,9 @@ export default (query: { [index: string]: string }, raids: Raid[]) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Extract raid filter
|
// Extract raid filter
|
||||||
|
const allRaids = raidGroups.flatMap((group) => group.raids)
|
||||||
const raidParam: string = query.raid
|
const raidParam: string = query.raid
|
||||||
const raid: Raid | undefined = raids.find((r) => r.slug === raidParam)
|
const raid: Raid | undefined = allRaids.find((r) => r.slug === raidParam)
|
||||||
|
|
||||||
// Return filter object
|
// Return filter object
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
export default (raids: Raid[]) => {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
raids: raids,
|
|
||||||
sortedRaids: groupedRaids,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue