Merge pull request #198 from jedmund/hovercard-revamp
Revamp hovercards
This commit is contained in:
commit
12976eab34
14 changed files with 743 additions and 313 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -54,6 +54,7 @@ public/images/job*
|
|||
public/images/awakening*
|
||||
public/images/ax*
|
||||
public/images/accessory*
|
||||
public/images/mastery*
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
.Character.HovercardContent {
|
||||
.title .Image {
|
||||
position: relative;
|
||||
|
||||
.Perpetuity {
|
||||
position: absolute;
|
||||
background-image: url('/icons/perpetuity/filled.svg');
|
||||
background-size: $unit-3x $unit-3x;
|
||||
z-index: 20;
|
||||
top: $unit-half * -1;
|
||||
right: $unit-3x;
|
||||
width: $unit-3x;
|
||||
height: $unit-3x;
|
||||
}
|
||||
}
|
||||
|
||||
.Mastery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-half;
|
||||
|
||||
.ExtendedMastery {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-half;
|
||||
|
||||
img {
|
||||
width: $unit-3x;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Awakening {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-half;
|
||||
|
||||
img {
|
||||
width: $unit-3x;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .Footer {
|
||||
// position: sticky;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// }
|
||||
}
|
||||
|
|
@ -1,17 +1,31 @@
|
|||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import jconv from 'jconv'
|
||||
|
||||
import * as HoverCard from '@radix-ui/react-hover-card'
|
||||
|
||||
import {
|
||||
Hovercard,
|
||||
HovercardContent,
|
||||
HovercardTrigger,
|
||||
} from '~components/Hovercard'
|
||||
import Button from '~components/Button'
|
||||
import WeaponLabelIcon from '~components/WeaponLabelIcon'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
|
||||
import {
|
||||
overMastery,
|
||||
aetherialMastery,
|
||||
permanentMastery,
|
||||
} from '~data/overMastery'
|
||||
import { characterAwakening } from '~data/awakening'
|
||||
import { ExtendedMastery } from '~types'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
gridCharacter: GridCharacter
|
||||
children: React.ReactNode
|
||||
onTriggerClick: () => void
|
||||
}
|
||||
|
||||
interface KeyNames {
|
||||
|
|
@ -43,10 +57,19 @@ const CharacterHovercard = (props: Props) => {
|
|||
]
|
||||
|
||||
const tintElement = Element[props.gridCharacter.object.element]
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridCharacter.object.name.en.replaceAll(
|
||||
' ',
|
||||
'_'
|
||||
)}`
|
||||
|
||||
function goTo() {
|
||||
const urlSafeName = props.gridCharacter.object.name.en.replaceAll(' ', '_')
|
||||
const url = `https://gbf.wiki/${urlSafeName}`
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const perpetuity = () => {
|
||||
if (props.gridCharacter && props.gridCharacter.perpetuity) {
|
||||
return <i className="Perpetuity" />
|
||||
}
|
||||
}
|
||||
|
||||
function characterImage() {
|
||||
let imgSrc = ''
|
||||
|
|
@ -66,59 +89,194 @@ const CharacterHovercard = (props: Props) => {
|
|||
return imgSrc
|
||||
}
|
||||
|
||||
function masteryElement(dictionary: ItemSkill[], mastery: ExtendedMastery) {
|
||||
const canonicalMastery = dictionary.find(
|
||||
(item) => item.id === mastery.modifier
|
||||
)
|
||||
|
||||
if (canonicalMastery) {
|
||||
return (
|
||||
<li className="ExtendedMastery" key={canonicalMastery.id}>
|
||||
<img
|
||||
alt={canonicalMastery.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/mastery/${canonicalMastery.slug}.png`}
|
||||
/>
|
||||
<span>
|
||||
<strong>{canonicalMastery.name[locale]}</strong>
|
||||
{`+${mastery.strength}${canonicalMastery.suffix}`}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const overMasterySection = () => {
|
||||
if (props.gridCharacter && props.gridCharacter.over_mastery) {
|
||||
return (
|
||||
<section className="Mastery">
|
||||
<h5 className={tintElement}>
|
||||
{t('modals.characters.subtitles.ring')}
|
||||
</h5>
|
||||
<ul>
|
||||
{[...Array(4)].map((e, i) => {
|
||||
const ringIndex = i + 1
|
||||
const ringStat: ExtendedMastery =
|
||||
props.gridCharacter.over_mastery[i]
|
||||
if (ringStat && ringStat.modifier && ringStat.modifier > 0) {
|
||||
if (ringIndex === 1 || ringIndex === 2) {
|
||||
return masteryElement(overMastery.a, ringStat)
|
||||
} else if (ringIndex === 3) {
|
||||
return masteryElement(overMastery.b, ringStat)
|
||||
} else {
|
||||
return masteryElement(overMastery.c, ringStat)
|
||||
}
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const aetherialMasterySection = () => {
|
||||
if (
|
||||
props.gridCharacter &&
|
||||
props.gridCharacter.aetherial_mastery &&
|
||||
props.gridCharacter.aetherial_mastery.modifier > 0
|
||||
) {
|
||||
return (
|
||||
<section className="Mastery">
|
||||
<h5 className={tintElement}>
|
||||
{t('modals.characters.subtitles.earring')}
|
||||
</h5>
|
||||
<ul>
|
||||
{masteryElement(
|
||||
aetherialMastery,
|
||||
props.gridCharacter.aetherial_mastery
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const permanentMasterySection = () => {
|
||||
if (props.gridCharacter && props.gridCharacter.perpetuity) {
|
||||
return (
|
||||
<section className="Mastery">
|
||||
<h5 className={tintElement}>
|
||||
{t('modals.characters.subtitles.permanent')}
|
||||
</h5>
|
||||
<ul>
|
||||
{[...Array(4)].map((e, i) => {
|
||||
return masteryElement(permanentMastery, {
|
||||
modifier: i + 1,
|
||||
strength: permanentMastery[i].maxValue,
|
||||
})
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const awakeningSection = () => {
|
||||
const gridAwakening = props.gridCharacter.awakening
|
||||
const awakening = characterAwakening.find(
|
||||
(awakening) => awakening.id === gridAwakening?.type
|
||||
)
|
||||
|
||||
if (gridAwakening && awakening) {
|
||||
return (
|
||||
<section className="Awakening">
|
||||
<h5 className={tintElement}>
|
||||
{t('modals.characters.subtitles.awakening')}
|
||||
</h5>
|
||||
<div>
|
||||
{gridAwakening.type > 1 ? (
|
||||
<img
|
||||
alt={awakening.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/character_${gridAwakening.type}.jpg`}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<span>
|
||||
<strong>{`${awakening.name[locale]}`}</strong>
|
||||
{`Lv${gridAwakening.level}`}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const wikiButton = (
|
||||
<Button
|
||||
className={tintElement}
|
||||
text={t('buttons.wiki')}
|
||||
onClick={goTo}
|
||||
contained={true}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content className="Weapon Hovercard">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridCharacter.object.name[locale]}</h4>
|
||||
<Hovercard openDelay={350}>
|
||||
<HovercardTrigger asChild onClick={props.onTriggerClick}>
|
||||
{props.children}
|
||||
</HovercardTrigger>
|
||||
<HovercardContent className="Character">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridCharacter.object.name[locale]}</h4>
|
||||
<div className="Image">
|
||||
{perpetuity()}
|
||||
<img
|
||||
alt={props.gridCharacter.object.name[locale]}
|
||||
src={characterImage()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon
|
||||
labelType={Element[props.gridCharacter.object.element]}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon
|
||||
labelType={Element[props.gridCharacter.object.element]}
|
||||
/>
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
Proficiency[
|
||||
props.gridCharacter.object.proficiency.proficiency1
|
||||
]
|
||||
}
|
||||
/>
|
||||
{props.gridCharacter.object.proficiency.proficiency2 ? (
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
Proficiency[
|
||||
props.gridCharacter.object.proficiency.proficiency1
|
||||
props.gridCharacter.object.proficiency.proficiency2
|
||||
]
|
||||
}
|
||||
/>
|
||||
{props.gridCharacter.object.proficiency.proficiency2 ? (
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
Proficiency[
|
||||
props.gridCharacter.object.proficiency.proficiency2
|
||||
]
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
ulb={props.gridCharacter.object.uncap.ulb || false}
|
||||
flb={props.gridCharacter.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
ulb={props.gridCharacter.object.uncap.ulb || false}
|
||||
flb={props.gridCharacter.object.uncap.flb || false}
|
||||
transcendenceStage={props.gridCharacter.transcendence_step}
|
||||
special={props.gridCharacter.object.special}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
|
||||
{t('buttons.wiki')}
|
||||
</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
</div>
|
||||
{wikiButton}
|
||||
{awakeningSection()}
|
||||
{overMasterySection()}
|
||||
{aetherialMasterySection()}
|
||||
{permanentMasterySection()}
|
||||
</HovercardContent>
|
||||
</Hovercard>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const CharacterUnit = ({
|
|||
setDetailsModalOpen(true)
|
||||
}
|
||||
|
||||
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
|
||||
function openSearchModal() {
|
||||
if (editable) setSearchModalOpen(true)
|
||||
}
|
||||
|
||||
|
|
@ -286,33 +286,50 @@ const CharacterUnit = ({
|
|||
}
|
||||
}
|
||||
|
||||
const image = (
|
||||
<div
|
||||
className="CharacterImage"
|
||||
onClick={openSearchModal}
|
||||
tabIndex={gridCharacter ? gridCharacter.position * 7 : 0}
|
||||
>
|
||||
const image = () => {
|
||||
let image = (
|
||||
<img
|
||||
alt={character?.name[locale]}
|
||||
className="grid_image"
|
||||
src={imageUrl}
|
||||
/>
|
||||
{editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className="CharacterImage"
|
||||
tabIndex={gridCharacter ? gridCharacter.position * 7 : 0}
|
||||
onClick={openSearchModal}
|
||||
>
|
||||
{image}
|
||||
{editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return gridCharacter ? (
|
||||
<CharacterHovercard
|
||||
gridCharacter={gridCharacter}
|
||||
onTriggerClick={openSearchModal}
|
||||
>
|
||||
{content}
|
||||
</CharacterHovercard>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
const unitContent = (
|
||||
<>
|
||||
<div className={classes}>
|
||||
{contextMenu()}
|
||||
{perpetuity()}
|
||||
{image}
|
||||
{image()}
|
||||
{gridCharacter && character ? (
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
|
|
@ -335,13 +352,7 @@ const CharacterUnit = ({
|
|||
</>
|
||||
)
|
||||
|
||||
const unitContentWithHovercard = (
|
||||
<CharacterHovercard gridCharacter={gridCharacter!}>
|
||||
{unitContent}
|
||||
</CharacterHovercard>
|
||||
)
|
||||
|
||||
return gridCharacter && !editable ? unitContentWithHovercard : unitContent
|
||||
return unitContent
|
||||
}
|
||||
|
||||
export default CharacterUnit
|
||||
|
|
|
|||
91
components/Hovercard/index.scss
Normal file
91
components/Hovercard/index.scss
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
.HovercardContent {
|
||||
animation: scaleIn $duration-zoom ease-out;
|
||||
transform-origin: var(--radix-hover-card-content-transform-origin);
|
||||
background: var(--dialog-bg);
|
||||
border-radius: $card-corner;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
max-height: 30vh;
|
||||
overflow-y: scroll;
|
||||
padding: $unit-2x;
|
||||
width: 300px;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
|
||||
h4 {
|
||||
flex-grow: 1;
|
||||
font-size: $font-medium;
|
||||
line-height: 1.2;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.subInfo {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
h5 {
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
opacity: 0.7;
|
||||
|
||||
&.wind {
|
||||
color: $wind-bg-20;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
color: $fire-bg-20;
|
||||
}
|
||||
|
||||
&.water {
|
||||
color: $water-bg-20;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
color: $earth-bg-20;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
color: $dark-bg-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
color: $light-bg-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.Button {
|
||||
display: block;
|
||||
padding: $unit * 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
31
components/Hovercard/index.tsx
Normal file
31
components/Hovercard/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React, { PropsWithChildren } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
|
||||
import './index.scss'
|
||||
|
||||
interface Props extends HoverCardPrimitive.HoverCardContentProps {}
|
||||
|
||||
export const Hovercard = HoverCardPrimitive.Root
|
||||
export const HovercardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
export const HovercardContent = ({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const classes = classNames(props.className, {
|
||||
HovercardContent: true,
|
||||
})
|
||||
return (
|
||||
<HoverCardPrimitive.Portal>
|
||||
<HoverCardPrimitive.Content
|
||||
{...props}
|
||||
className={classes}
|
||||
sideOffset={4}
|
||||
collisionPadding={{ top: 16, left: 16, right: 16, bottom: 16 }}
|
||||
>
|
||||
{children}
|
||||
</HoverCardPrimitive.Content>
|
||||
</HoverCardPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,8 +2,12 @@ import React from 'react'
|
|||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import * as HoverCard from '@radix-ui/react-hover-card'
|
||||
|
||||
import {
|
||||
Hovercard,
|
||||
HovercardContent,
|
||||
HovercardTrigger,
|
||||
} from '~components/Hovercard'
|
||||
import Button from '~components/Button'
|
||||
import WeaponLabelIcon from '~components/WeaponLabelIcon'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
|
||||
|
|
@ -12,6 +16,7 @@ import './index.scss'
|
|||
interface Props {
|
||||
gridSummon: GridSummon
|
||||
children: React.ReactNode
|
||||
onTriggerClick: () => void
|
||||
}
|
||||
|
||||
const SummonHovercard = (props: Props) => {
|
||||
|
|
@ -23,10 +28,13 @@ const SummonHovercard = (props: Props) => {
|
|||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
|
||||
const tintElement = Element[props.gridSummon.object.element]
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridSummon.object.name.en.replaceAll(
|
||||
' ',
|
||||
'_'
|
||||
)}`
|
||||
|
||||
function goTo() {
|
||||
const urlSafeName = props.gridSummon.object.name.en.replaceAll(' ', '_')
|
||||
const url = `https://gbf.wiki/${urlSafeName}`
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function summonImage() {
|
||||
let imgSrc = ''
|
||||
|
|
@ -59,40 +67,46 @@ const SummonHovercard = (props: Props) => {
|
|||
return imgSrc
|
||||
}
|
||||
|
||||
const wikiButton = (
|
||||
<Button
|
||||
className={tintElement}
|
||||
text={t('buttons.wiki')}
|
||||
onClick={goTo}
|
||||
contained={true}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content className="Weapon Hovercard">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridSummon.object.name[locale]}</h4>
|
||||
<img
|
||||
alt={props.gridSummon.object.name[locale]}
|
||||
src={summonImage()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon
|
||||
labelType={Element[props.gridSummon.object.element]}
|
||||
/>
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={props.gridSummon.object.uncap.ulb || false}
|
||||
flb={props.gridSummon.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
<Hovercard openDelay={350}>
|
||||
<HovercardTrigger asChild onClick={props.onTriggerClick}>
|
||||
{props.children}
|
||||
</HovercardTrigger>
|
||||
<HovercardContent className="Summon">
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridSummon.object.name[locale]}</h4>
|
||||
<img
|
||||
alt={props.gridSummon.object.name[locale]}
|
||||
src={summonImage()}
|
||||
/>
|
||||
</div>
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
|
||||
{t('buttons.wiki')}
|
||||
</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
<WeaponLabelIcon
|
||||
labelType={Element[props.gridSummon.object.element]}
|
||||
/>
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={props.gridSummon.object.uncap.ulb || false}
|
||||
flb={props.gridSummon.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{wikiButton}
|
||||
</HovercardContent>
|
||||
</Hovercard>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ const SummonUnit = ({
|
|||
})
|
||||
|
||||
// Methods: Open layer
|
||||
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
|
||||
function openSearchModal() {
|
||||
if (editable) setSearchModalOpen(true)
|
||||
}
|
||||
|
||||
|
|
@ -223,8 +223,8 @@ const SummonUnit = ({
|
|||
}
|
||||
|
||||
// Methods: Core element rendering
|
||||
const image = (
|
||||
<div className="SummonImage" onClick={openSearchModal}>
|
||||
const image = () => {
|
||||
let image = (
|
||||
<img
|
||||
alt={summon?.name[locale]}
|
||||
className={classNames({
|
||||
|
|
@ -233,21 +233,35 @@ const SummonUnit = ({
|
|||
})}
|
||||
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
|
||||
/>
|
||||
{editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
const content = (
|
||||
<div className="SummonImage" onClick={openSearchModal}>
|
||||
{image}
|
||||
{editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return gridSummon ? (
|
||||
<SummonHovercard gridSummon={gridSummon} onTriggerClick={openSearchModal}>
|
||||
{content}
|
||||
</SummonHovercard>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
const unitContent = (
|
||||
<>
|
||||
<div className={classes}>
|
||||
{contextMenu()}
|
||||
{image}
|
||||
{image()}
|
||||
{gridSummon ? (
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
|
|
@ -271,11 +285,7 @@ const SummonUnit = ({
|
|||
</>
|
||||
)
|
||||
|
||||
const unitContentWithHovercard = (
|
||||
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
|
||||
)
|
||||
|
||||
return gridSummon && !editable ? unitContentWithHovercard : unitContent
|
||||
return unitContent
|
||||
}
|
||||
|
||||
export default SummonUnit
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
.Weapon.Hovercard {
|
||||
.Weapon.HovercardContent {
|
||||
.skills {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-right: $unit * 2;
|
||||
padding-right: $unit-2x;
|
||||
|
||||
.axSkill {
|
||||
align-items: center;
|
||||
|
|
@ -36,6 +36,26 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: $normal;
|
||||
gap: calc($unit / 2);
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.awakening {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-half;
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-half;
|
||||
|
||||
img {
|
||||
width: $unit-4x;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,24 @@ import React from 'react'
|
|||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import * as HoverCard from '@radix-ui/react-hover-card'
|
||||
|
||||
import {
|
||||
Hovercard,
|
||||
HovercardContent,
|
||||
HovercardTrigger,
|
||||
} from '~components/Hovercard'
|
||||
import Button from '~components/Button'
|
||||
import WeaponLabelIcon from '~components/WeaponLabelIcon'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
|
||||
import ax from '~data/ax'
|
||||
import { weaponAwakening } from '~data/awakening'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
gridWeapon: GridWeapon
|
||||
children: React.ReactNode
|
||||
onTriggerClick: () => void
|
||||
}
|
||||
|
||||
interface KeyNames {
|
||||
|
|
@ -26,10 +32,11 @@ interface KeyNames {
|
|||
|
||||
const WeaponHovercard = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
|
||||
const Proficiency = [
|
||||
'none',
|
||||
|
|
@ -67,11 +74,19 @@ const WeaponHovercard = (props: Props) => {
|
|||
props.gridWeapon.object.element == 0 && props.gridWeapon.element
|
||||
? Element[props.gridWeapon.element]
|
||||
: Element[props.gridWeapon.object.element]
|
||||
|
||||
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(
|
||||
' ',
|
||||
'_'
|
||||
)}`
|
||||
|
||||
function goTo() {
|
||||
const urlSafeName = props.gridWeapon.object.name.en.replaceAll(' ', '_')
|
||||
const url = `https://gbf.wiki/${urlSafeName}`
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const hovercardSide = () => {
|
||||
if (props.gridWeapon.position == -1) return 'right'
|
||||
else if ([6, 7, 8, 9, 10, 11].includes(props.gridWeapon.position))
|
||||
|
|
@ -129,6 +144,33 @@ const WeaponHovercard = (props: Props) => {
|
|||
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
|
||||
}
|
||||
|
||||
const awakeningSection = () => {
|
||||
const gridAwakening = props.gridWeapon.awakening
|
||||
const awakening = weaponAwakening.find(
|
||||
(awakening) => awakening.id === gridAwakening?.type
|
||||
)
|
||||
|
||||
if (gridAwakening && awakening) {
|
||||
return (
|
||||
<section className="awakening">
|
||||
<h5 className={tintElement}>
|
||||
{t('modals.weapon.subtitles.awakening')}
|
||||
</h5>
|
||||
<div>
|
||||
<img
|
||||
alt={awakening.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${gridAwakening.type}.png`}
|
||||
/>
|
||||
<span>
|
||||
<strong>{`${awakening.name[locale]}`}</strong>
|
||||
{`Lv${gridAwakening.level}`}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const keysSection = (
|
||||
<section className="weaponKeys">
|
||||
{WeaponKeyNames[props.gridWeapon.object.series] ? (
|
||||
|
|
@ -188,65 +230,71 @@ const WeaponHovercard = (props: Props) => {
|
|||
</section>
|
||||
)
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger>{props.children}</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}>
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridWeapon.object.name[locale]}</h4>
|
||||
<img
|
||||
alt={props.gridWeapon.object.name[locale]}
|
||||
src={weaponImage()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
{props.gridWeapon.object.element !== 0 ||
|
||||
(props.gridWeapon.object.element === 0 &&
|
||||
props.gridWeapon.element != null) ? (
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
props.gridWeapon.object.element === 0 &&
|
||||
props.gridWeapon.element !== 0
|
||||
? Element[props.gridWeapon.element]
|
||||
: Element[props.gridWeapon.object.element]
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<WeaponLabelIcon
|
||||
labelType={Proficiency[props.gridWeapon.object.proficiency]}
|
||||
/>
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
ulb={props.gridWeapon.object.uncap.ulb || false}
|
||||
flb={props.gridWeapon.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
const wikiButton = (
|
||||
<Button
|
||||
className={tintElement}
|
||||
text={t('buttons.wiki')}
|
||||
onClick={goTo}
|
||||
contained={true}
|
||||
/>
|
||||
)
|
||||
|
||||
{props.gridWeapon.object.ax &&
|
||||
props.gridWeapon.ax &&
|
||||
props.gridWeapon.ax[0].modifier &&
|
||||
props.gridWeapon.ax[0].strength
|
||||
? axSection
|
||||
: ''}
|
||||
{props.gridWeapon.weapon_keys &&
|
||||
props.gridWeapon.weapon_keys.length > 0
|
||||
? keysSection
|
||||
: ''}
|
||||
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
|
||||
{t('buttons.wiki')}
|
||||
</a>
|
||||
<HoverCard.Arrow />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
return (
|
||||
<Hovercard openDelay={350}>
|
||||
<HovercardTrigger asChild onClick={props.onTriggerClick}>
|
||||
{props.children}
|
||||
</HovercardTrigger>
|
||||
<HovercardContent className="Weapon" side={hovercardSide()}>
|
||||
<div className="top">
|
||||
<div className="title">
|
||||
<h4>{props.gridWeapon.object.name[locale]}</h4>
|
||||
<img
|
||||
alt={props.gridWeapon.object.name[locale]}
|
||||
src={weaponImage()}
|
||||
/>
|
||||
</div>
|
||||
<div className="subInfo">
|
||||
<div className="icons">
|
||||
{props.gridWeapon.object.element !== 0 ||
|
||||
(props.gridWeapon.object.element === 0 &&
|
||||
props.gridWeapon.element != null) ? (
|
||||
<WeaponLabelIcon
|
||||
labelType={
|
||||
props.gridWeapon.object.element === 0 &&
|
||||
props.gridWeapon.element !== 0
|
||||
? Element[props.gridWeapon.element]
|
||||
: Element[props.gridWeapon.object.element]
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<WeaponLabelIcon
|
||||
labelType={Proficiency[props.gridWeapon.object.proficiency]}
|
||||
/>
|
||||
</div>
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
ulb={props.gridWeapon.object.uncap.ulb || false}
|
||||
flb={props.gridWeapon.object.uncap.flb || false}
|
||||
special={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{props.gridWeapon.object.ax &&
|
||||
props.gridWeapon.ax &&
|
||||
props.gridWeapon.ax[0].modifier &&
|
||||
props.gridWeapon.ax[0].strength
|
||||
? axSection
|
||||
: ''}
|
||||
{awakeningSection()}
|
||||
{props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0
|
||||
? keysSection
|
||||
: ''}
|
||||
{wikiButton}
|
||||
</HovercardContent>
|
||||
</Hovercard>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ const WeaponModal = ({
|
|||
if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon
|
||||
else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||
|
||||
if (onOpenChange) onOpenChange(false)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const WeaponUnit = ({
|
|||
setDetailsModalOpen(true)
|
||||
}
|
||||
|
||||
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
|
||||
function openSearchModal() {
|
||||
if (editable) setSearchModalOpen(true)
|
||||
}
|
||||
|
||||
|
|
@ -509,17 +509,8 @@ const WeaponUnit = ({
|
|||
}
|
||||
|
||||
// Methods: Core element rendering
|
||||
const image = (
|
||||
<div className="WeaponImage" onClick={openSearchModal}>
|
||||
<div className="Modifiers">
|
||||
{awakeningImage()}
|
||||
<div className="Skills">
|
||||
{axImages()}
|
||||
{telumaImages()}
|
||||
{opusImages()}
|
||||
{ultimaImages()}
|
||||
</div>
|
||||
</div>
|
||||
const image = () => {
|
||||
const image = (
|
||||
<img
|
||||
alt={weapon?.name[locale]}
|
||||
className={classNames({
|
||||
|
|
@ -528,21 +519,44 @@ const WeaponUnit = ({
|
|||
})}
|
||||
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
|
||||
/>
|
||||
{editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
const content = (
|
||||
<div className="WeaponImage" onClick={openSearchModal}>
|
||||
<div className="Modifiers">
|
||||
{awakeningImage()}
|
||||
<div className="Skills">
|
||||
{axImages()}
|
||||
{telumaImages()}
|
||||
{opusImages()}
|
||||
{ultimaImages()}
|
||||
</div>
|
||||
</div>
|
||||
{image}
|
||||
{editable ? (
|
||||
<span className="icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return gridWeapon ? (
|
||||
<WeaponHovercard gridWeapon={gridWeapon} onTriggerClick={openSearchModal}>
|
||||
{content}
|
||||
</WeaponHovercard>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
const unitContent = (
|
||||
<>
|
||||
<div className={classes}>
|
||||
{contextMenu()}
|
||||
{image}
|
||||
{image()}
|
||||
{gridWeapon && weapon ? (
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
|
|
@ -562,11 +576,7 @@ const WeaponUnit = ({
|
|||
</>
|
||||
)
|
||||
|
||||
const unitContentWithHovercard = (
|
||||
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard>
|
||||
)
|
||||
|
||||
return gridWeapon && !editable ? unitContentWithHovercard : unitContent
|
||||
return unitContent
|
||||
}
|
||||
|
||||
export default WeaponUnit
|
||||
|
|
|
|||
|
|
@ -330,3 +330,54 @@ export const aetherialMastery: ItemSkill[] = [
|
|||
fractional: false,
|
||||
},
|
||||
]
|
||||
|
||||
export const permanentMastery: ItemSkill[] = [
|
||||
{
|
||||
name: {
|
||||
en: 'Extended Mastery Star Cap',
|
||||
ja: 'LB強化回数上限',
|
||||
},
|
||||
id: 1,
|
||||
slug: 'star-cap',
|
||||
minValue: 10,
|
||||
maxValue: 10,
|
||||
suffix: '',
|
||||
fractional: false,
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'ATK',
|
||||
ja: '攻撃',
|
||||
},
|
||||
id: 2,
|
||||
slug: 'atk',
|
||||
minValue: 10,
|
||||
maxValue: 10,
|
||||
suffix: '%',
|
||||
fractional: false,
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'HP',
|
||||
ja: 'HP',
|
||||
},
|
||||
id: 3,
|
||||
slug: 'hp',
|
||||
minValue: 10,
|
||||
maxValue: 10,
|
||||
suffix: '',
|
||||
fractional: false,
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'DMG Cap',
|
||||
ja: 'ダメージ上限',
|
||||
},
|
||||
id: 4,
|
||||
slug: 'dmg-cap',
|
||||
minValue: 5,
|
||||
maxValue: 5,
|
||||
suffix: '%',
|
||||
fractional: false,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -200,98 +200,6 @@ select {
|
|||
}
|
||||
}
|
||||
|
||||
.Hovercard {
|
||||
background: #222;
|
||||
border-radius: $unit;
|
||||
color: $grey-100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 2;
|
||||
padding: $unit * 2;
|
||||
width: 300px;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
|
||||
.title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
|
||||
h4 {
|
||||
flex-grow: 1;
|
||||
font-size: $font-medium;
|
||||
line-height: 1.2;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.subInfo {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit * 2;
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.UncapIndicator {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
h5 {
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
opacity: 0.7;
|
||||
|
||||
&.wind {
|
||||
color: $wind-bg-20;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
color: $fire-bg-20;
|
||||
}
|
||||
|
||||
&.water {
|
||||
color: $water-bg-20;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
color: $earth-bg-20;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
color: $dark-bg-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
color: $light-bg-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.Button {
|
||||
display: block;
|
||||
padding: $unit * 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#Teams,
|
||||
#Profile {
|
||||
display: flex;
|
||||
|
|
@ -447,17 +355,25 @@ i.tag {
|
|||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.8;
|
||||
65% {
|
||||
opacity: 0.65;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.75;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
80% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
90% {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
opacity: 0.9;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue