Make custom Hovercard component and migrate

This commit is contained in:
Justin Edmund 2023-01-29 18:30:38 -08:00
parent e96c3e7fb8
commit 7e7d89b01d
10 changed files with 388 additions and 304 deletions

View file

@ -2,8 +2,11 @@ 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 WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
@ -12,6 +15,7 @@ import './index.scss'
interface Props {
gridCharacter: GridCharacter
children: React.ReactNode
onTriggerClick: () => void
}
interface KeyNames {
@ -67,58 +71,57 @@ const CharacterHovercard = (props: Props) => {
}
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>
<img
alt={props.gridCharacter.object.name[locale]}
src={characterImage()}
<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>
<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}
special={false}
/>
</div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
</HovercardContent>
</Hovercard>
)
}

View file

@ -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>
)
)
if (gridCharacter) {
image = (
<CharacterHovercard
gridCharacter={gridCharacter}
onTriggerClick={openSearchModal}
>
{image}
</CharacterHovercard>
)
}
return (
<div
className="CharacterImage"
tabIndex={gridCharacter ? gridCharacter.position * 7 : 0}
onClick={openSearchModal}
>
{image}
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
}
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

View file

@ -0,0 +1,93 @@
.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;
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;
}
.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;
}
}

View 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>
)
}

View file

@ -2,8 +2,11 @@ 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 WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
@ -12,6 +15,7 @@ import './index.scss'
interface Props {
gridSummon: GridSummon
children: React.ReactNode
onTriggerClick: () => void
}
const SummonHovercard = (props: Props) => {
@ -60,39 +64,38 @@ const SummonHovercard = (props: Props) => {
}
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>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')}
</a>
</HovercardContent>
</Hovercard>
)
}

View file

@ -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,38 @@ const SummonUnit = ({
})}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
/>
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
)
if (gridSummon) {
image = (
<SummonHovercard
gridSummon={gridSummon}
onTriggerClick={openSearchModal}
>
{image}
</SummonHovercard>
)
}
return (
<div className="SummonImage" onClick={openSearchModal}>
{image}
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
}
const unitContent = (
<>
<div className={classes}>
{contextMenu()}
{image}
{image()}
{gridSummon ? (
<UncapIndicator
type="summon"
@ -271,11 +288,7 @@ const SummonUnit = ({
</>
)
const unitContentWithHovercard = (
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
)
return gridSummon && !editable ? unitContentWithHovercard : unitContent
return unitContent
}
export default SummonUnit

View file

@ -1,4 +1,4 @@
.Weapon.Hovercard {
.Weapon.HovercardContent {
.skills {
display: flex;
flex-direction: row;

View file

@ -2,8 +2,11 @@ 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 WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator'
@ -14,6 +17,7 @@ import './index.scss'
interface Props {
gridWeapon: GridWeapon
children: React.ReactNode
onTriggerClick: () => void
}
interface KeyNames {
@ -26,10 +30,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,6 +72,7 @@ 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(
' ',
'_'
@ -189,64 +195,62 @@ const WeaponHovercard = (props: Props) => {
)
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>
<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
: ''}
{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>
{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>
</HovercardContent>
</Hovercard>
)
}

View file

@ -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

View file

@ -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;