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 { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' 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 WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
@ -12,6 +15,7 @@ import './index.scss'
interface Props { interface Props {
gridCharacter: GridCharacter gridCharacter: GridCharacter
children: React.ReactNode children: React.ReactNode
onTriggerClick: () => void
} }
interface KeyNames { interface KeyNames {
@ -67,58 +71,57 @@ const CharacterHovercard = (props: Props) => {
} }
return ( return (
<HoverCard.Root> <Hovercard openDelay={350}>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger> <HovercardTrigger asChild onClick={props.onTriggerClick}>
<HoverCard.Portal> {props.children}
<HoverCard.Content className="Weapon Hovercard"> </HovercardTrigger>
<div className="top"> <HovercardContent className="Character">
<div className="title"> <div className="top">
<h4>{props.gridCharacter.object.name[locale]}</h4> <div className="title">
<img <h4>{props.gridCharacter.object.name[locale]}</h4>
alt={props.gridCharacter.object.name[locale]} <img
src={characterImage()} alt={props.gridCharacter.object.name[locale]}
src={characterImage()}
/>
</div>
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon
labelType={Element[props.gridCharacter.object.element]}
/> />
</div> <WeaponLabelIcon
<div className="subInfo"> labelType={
<div className="icons"> Proficiency[
<WeaponLabelIcon props.gridCharacter.object.proficiency.proficiency1
labelType={Element[props.gridCharacter.object.element]} ]
/> }
/>
{props.gridCharacter.object.proficiency.proficiency2 ? (
<WeaponLabelIcon <WeaponLabelIcon
labelType={ labelType={
Proficiency[ 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> </div>
<UncapIndicator
type="character"
ulb={props.gridCharacter.object.uncap.ulb || false}
flb={props.gridCharacter.object.uncap.flb || false}
special={false}
/>
</div> </div>
</div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new"> <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
{t('buttons.wiki')} {t('buttons.wiki')}
</a> </a>
<HoverCard.Arrow /> </HovercardContent>
</HoverCard.Content> </Hovercard>
</HoverCard.Portal>
</HoverCard.Root>
) )
} }

View file

@ -95,7 +95,7 @@ const CharacterUnit = ({
setDetailsModalOpen(true) setDetailsModalOpen(true)
} }
function openSearchModal(event: MouseEvent<HTMLDivElement>) { function openSearchModal() {
if (editable) setSearchModalOpen(true) if (editable) setSearchModalOpen(true)
} }
@ -286,33 +286,50 @@ const CharacterUnit = ({
} }
} }
const image = ( const image = () => {
<div let image = (
className="CharacterImage"
onClick={openSearchModal}
tabIndex={gridCharacter ? gridCharacter.position * 7 : 0}
>
<img <img
alt={character?.name[locale]} alt={character?.name[locale]}
className="grid_image" className="grid_image"
src={imageUrl} src={imageUrl}
/> />
{editable ? ( )
<span className="icon">
<PlusIcon /> if (gridCharacter) {
</span> image = (
) : ( <CharacterHovercard
'' gridCharacter={gridCharacter}
)} onTriggerClick={openSearchModal}
</div> >
) {image}
</CharacterHovercard>
)
}
return (
<div
className="CharacterImage"
tabIndex={gridCharacter ? gridCharacter.position * 7 : 0}
onClick={openSearchModal}
>
{image}
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
}
const unitContent = ( const unitContent = (
<> <>
<div className={classes}> <div className={classes}>
{contextMenu()} {contextMenu()}
{perpetuity()} {perpetuity()}
{image} {image()}
{gridCharacter && character ? ( {gridCharacter && character ? (
<UncapIndicator <UncapIndicator
type="character" type="character"
@ -335,13 +352,7 @@ const CharacterUnit = ({
</> </>
) )
const unitContentWithHovercard = ( return unitContent
<CharacterHovercard gridCharacter={gridCharacter!}>
{unitContent}
</CharacterHovercard>
)
return gridCharacter && !editable ? unitContentWithHovercard : unitContent
} }
export default CharacterUnit 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 { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' 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 WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
@ -12,6 +15,7 @@ import './index.scss'
interface Props { interface Props {
gridSummon: GridSummon gridSummon: GridSummon
children: React.ReactNode children: React.ReactNode
onTriggerClick: () => void
} }
const SummonHovercard = (props: Props) => { const SummonHovercard = (props: Props) => {
@ -60,39 +64,38 @@ const SummonHovercard = (props: Props) => {
} }
return ( return (
<HoverCard.Root> <Hovercard openDelay={350}>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger> <HovercardTrigger asChild onClick={props.onTriggerClick}>
<HoverCard.Portal> {props.children}
<HoverCard.Content className="Weapon Hovercard"> </HovercardTrigger>
<div className="top"> <HovercardContent className="Summon">
<div className="title"> <div className="top">
<h4>{props.gridSummon.object.name[locale]}</h4> <div className="title">
<img <h4>{props.gridSummon.object.name[locale]}</h4>
alt={props.gridSummon.object.name[locale]} <img
src={summonImage()} alt={props.gridSummon.object.name[locale]}
/> src={summonImage()}
</div> />
<div className="subInfo">
<div className="icons">
<WeaponLabelIcon
labelType={Element[props.gridSummon.object.element]}
/>
</div>
<UncapIndicator
type="summon"
ulb={props.gridSummon.object.uncap.ulb || false}
flb={props.gridSummon.object.uncap.flb || false}
special={false}
/>
</div>
</div> </div>
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new"> <div className="subInfo">
{t('buttons.wiki')} <div className="icons">
</a> <WeaponLabelIcon
<HoverCard.Arrow /> labelType={Element[props.gridSummon.object.element]}
</HoverCard.Content> />
</HoverCard.Portal> </div>
</HoverCard.Root> <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 // Methods: Open layer
function openSearchModal(event: MouseEvent<HTMLDivElement>) { function openSearchModal() {
if (editable) setSearchModalOpen(true) if (editable) setSearchModalOpen(true)
} }
@ -223,8 +223,8 @@ const SummonUnit = ({
} }
// Methods: Core element rendering // Methods: Core element rendering
const image = ( const image = () => {
<div className="SummonImage" onClick={openSearchModal}> let image = (
<img <img
alt={summon?.name[locale]} alt={summon?.name[locale]}
className={classNames({ className={classNames({
@ -233,21 +233,38 @@ const SummonUnit = ({
})} })}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()} src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
/> />
{editable ? ( )
<span className="icon">
<PlusIcon /> if (gridSummon) {
</span> image = (
) : ( <SummonHovercard
'' gridSummon={gridSummon}
)} onTriggerClick={openSearchModal}
</div> >
) {image}
</SummonHovercard>
)
}
return (
<div className="SummonImage" onClick={openSearchModal}>
{image}
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
}
const unitContent = ( const unitContent = (
<> <>
<div className={classes}> <div className={classes}>
{contextMenu()} {contextMenu()}
{image} {image()}
{gridSummon ? ( {gridSummon ? (
<UncapIndicator <UncapIndicator
type="summon" type="summon"
@ -271,11 +288,7 @@ const SummonUnit = ({
</> </>
) )
const unitContentWithHovercard = ( return unitContent
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
)
return gridSummon && !editable ? unitContentWithHovercard : unitContent
} }
export default SummonUnit export default SummonUnit

View file

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

View file

@ -2,8 +2,11 @@ import React from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' 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 WeaponLabelIcon from '~components/WeaponLabelIcon'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
@ -14,6 +17,7 @@ import './index.scss'
interface Props { interface Props {
gridWeapon: GridWeapon gridWeapon: GridWeapon
children: React.ReactNode children: React.ReactNode
onTriggerClick: () => void
} }
interface KeyNames { interface KeyNames {
@ -26,10 +30,11 @@ interface KeyNames {
const WeaponHovercard = (props: Props) => { const WeaponHovercard = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common')
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light'] const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const Proficiency = [ const Proficiency = [
'none', 'none',
@ -67,6 +72,7 @@ const WeaponHovercard = (props: Props) => {
props.gridWeapon.object.element == 0 && props.gridWeapon.element props.gridWeapon.object.element == 0 && props.gridWeapon.element
? Element[props.gridWeapon.element] ? Element[props.gridWeapon.element]
: Element[props.gridWeapon.object.element] : Element[props.gridWeapon.object.element]
const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll( const wikiUrl = `https://gbf.wiki/${props.gridWeapon.object.name.en.replaceAll(
' ', ' ',
'_' '_'
@ -189,64 +195,62 @@ const WeaponHovercard = (props: Props) => {
) )
return ( return (
<HoverCard.Root> <Hovercard openDelay={350}>
<HoverCard.Trigger>{props.children}</HoverCard.Trigger> <HovercardTrigger asChild onClick={props.onTriggerClick}>
<HoverCard.Portal> {props.children}
<HoverCard.Content className="Weapon Hovercard" side={hovercardSide()}> </HovercardTrigger>
<div className="top"> <HovercardContent className="Weapon" side={hovercardSide()}>
<div className="title"> <div className="top">
<h4>{props.gridWeapon.object.name[locale]}</h4> <div className="title">
<img <h4>{props.gridWeapon.object.name[locale]}</h4>
alt={props.gridWeapon.object.name[locale]} <img
src={weaponImage()} 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> </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.object.ax &&
props.gridWeapon.ax && props.gridWeapon.ax &&
props.gridWeapon.ax[0].modifier && props.gridWeapon.ax[0].modifier &&
props.gridWeapon.ax[0].strength props.gridWeapon.ax[0].strength
? axSection ? axSection
: ''} : ''}
{props.gridWeapon.weapon_keys && {props.gridWeapon.weapon_keys && props.gridWeapon.weapon_keys.length > 0
props.gridWeapon.weapon_keys.length > 0 ? keysSection
? keysSection : ''}
: ''} <a className={`Button ${tintElement}`} href={wikiUrl} target="_new">
<a className={`Button ${tintElement}`} href={wikiUrl} target="_new"> {t('buttons.wiki')}
{t('buttons.wiki')} </a>
</a> </HovercardContent>
<HoverCard.Arrow /> </Hovercard>
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
) )
} }

View file

@ -98,7 +98,7 @@ const WeaponUnit = ({
setDetailsModalOpen(true) setDetailsModalOpen(true)
} }
function openSearchModal(event: MouseEvent<HTMLDivElement>) { function openSearchModal() {
if (editable) setSearchModalOpen(true) if (editable) setSearchModalOpen(true)
} }
@ -509,17 +509,8 @@ const WeaponUnit = ({
} }
// Methods: Core element rendering // Methods: Core element rendering
const image = ( const image = () => {
<div className="WeaponImage" onClick={openSearchModal}> const image = (
<div className="Modifiers">
{awakeningImage()}
<div className="Skills">
{axImages()}
{telumaImages()}
{opusImages()}
{ultimaImages()}
</div>
</div>
<img <img
alt={weapon?.name[locale]} alt={weapon?.name[locale]}
className={classNames({ className={classNames({
@ -528,21 +519,44 @@ const WeaponUnit = ({
})} })}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()} src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
/> />
{editable ? ( )
<span className="icon">
<PlusIcon /> const content = (
</span> <div className="WeaponImage" onClick={openSearchModal}>
) : ( <div className="Modifiers">
'' {awakeningImage()}
)} <div className="Skills">
</div> {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 = ( const unitContent = (
<> <>
<div className={classes}> <div className={classes}>
{contextMenu()} {contextMenu()}
{image} {image()}
{gridWeapon && weapon ? ( {gridWeapon && weapon ? (
<UncapIndicator <UncapIndicator
type="weapon" type="weapon"
@ -562,11 +576,7 @@ const WeaponUnit = ({
</> </>
) )
const unitContentWithHovercard = ( return unitContent
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard>
)
return gridWeapon && !editable ? unitContentWithHovercard : unitContent
} }
export default WeaponUnit 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, #Teams,
#Profile { #Profile {
display: flex; display: flex;
@ -447,17 +355,25 @@ i.tag {
opacity: 0.6; opacity: 0.6;
transform: scale(1); transform: scale(1);
} }
70% { 65% {
opacity: 0.8; opacity: 0.65;
transform: scale(1.1); transform: scale(1.1);
} }
70% {
opacity: 0.7;
transform: scale(1);
}
75% {
opacity: 0.75;
transform: scale(0.98);
}
80% { 80% {
opacity: 0.8; opacity: 0.8;
transform: scale(1); transform: scale(1.02);
} }
90% { 90% {
opacity: 0.8; opacity: 0.9;
transform: scale(0.95); transform: scale(0.96);
} }
100% { 100% {
opacity: 1; opacity: 1;