Added GuidebooksGrid and GuidebookUnit

These are the display components for Guidebooks in the WeaponGrid
This commit is contained in:
Justin Edmund 2023-04-19 00:27:39 -07:00
parent ba52ba4fb0
commit 76aadfbc15
4 changed files with 450 additions and 0 deletions

View file

@ -0,0 +1,109 @@
.GuidebookUnit {
align-items: center;
display: flex;
flex-direction: column;
gap: $unit-half;
position: relative;
width: 100%;
height: auto;
z-index: 0;
@include breakpoint(tablet) {
min-height: auto;
}
.Button {
pointer-events: none;
opacity: 0;
z-index: 10;
}
&:hover .Button,
.Button.Clicked {
pointer-events: initial;
opacity: 1;
}
&.editable .GuidebookImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
cursor: pointer;
transform: $scale-wide;
}
&.empty {
min-height: auto;
}
&.filled h3 {
display: block;
}
&.filled ul {
display: flex;
}
& h3,
& ul {
display: none;
}
h3 {
color: var(--text-primary);
font-size: $font-button;
font-weight: $normal;
line-height: 1.1;
margin: 0;
text-align: center;
}
.GuidebookImage {
background: var(--extra-purple-card-bg);
border: 1px solid rgba(0, 0, 0, 0);
border-radius: $unit;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: calc($unit / 4);
overflow: hidden;
position: relative;
transition: $duration-zoom all ease-in-out;
img {
position: relative;
width: 100%;
z-index: 2;
&.Placeholder {
opacity: 0;
}
}
.icon {
position: absolute;
height: $unit * 3;
width: $unit * 3;
z-index: 1;
svg {
transition: $duration-color-fade fill ease-in-out;
fill: var(--extra-purple-secondary);
}
}
}
.GuidebookName {
font-size: $font-name;
line-height: 1.2;
@include breakpoint(phone) {
font-size: $font-tiny;
}
}
.GuidebookDescription {
font-size: $font-small;
line-height: 1.2;
text-align: center;
}
}

View file

@ -0,0 +1,201 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import classNames from 'classnames'
import Alert from '~components/common/Alert'
import SearchModal from '~components/search/SearchModal'
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
} from '~components/common/ContextMenu'
import ContextMenuItem from '~components/common/ContextMenuItem'
import Button from '~components/common/Button'
import type { SearchableObject } from '~types'
import PlusIcon from '~public/icons/Add.svg'
import SettingsIcon from '~public/icons/Settings.svg'
import './index.scss'
interface Props {
guidebook: Guidebook | undefined
position: number
editable: boolean
removeGuidebook: (id: string) => void
updateObject: (object: SearchableObject, position: number) => void
}
const GuidebookUnit = ({
guidebook,
position,
editable,
removeGuidebook: sendGuidebookToRemove,
updateObject,
}: Props) => {
// Translations and locale
const { t } = useTranslation('common')
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// State: UI
const [searchModalOpen, setSearchModalOpen] = useState(false)
const [contextMenuOpen, setContextMenuOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false)
// State: Other
const [imageUrl, setImageUrl] = useState('')
// Classes
const classes = classNames({
GuidebookUnit: true,
editable: editable,
filled: guidebook !== undefined,
empty: guidebook == undefined,
})
const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
})
// Hooks
useEffect(() => {
generateImageUrl()
}, [guidebook])
// Methods: Open layer
function openSearchModal() {
if (editable) setSearchModalOpen(true)
}
function openRemoveGuidebookAlert() {
setAlertOpen(true)
}
// Methods: Handle button clicked
function handleButtonClicked() {
setContextMenuOpen(!contextMenuOpen)
}
// Methods: Handle open change
function handleContextMenuOpenChange(open: boolean) {
if (!open) setContextMenuOpen(false)
}
function handleSearchModalOpenChange(open: boolean) {
setSearchModalOpen(open)
}
// Methods: Mutate data
function removeGuidebook() {
if (guidebook) sendGuidebookToRemove(guidebook.id)
setAlertOpen(false)
}
// Methods: Image string generation
function generateImageUrl() {
let imgSrc = guidebook
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/guidebooks/book_${guidebook.granblue_id}.png`
: ''
setImageUrl(imgSrc)
}
const placeholderImageUrl = '/images/placeholders/placeholder-guidebook.png'
// Methods: Layer element rendering
const contextMenu = () => {
if (editable && guidebook) {
return (
<>
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<Button
leftAccessoryIcon={<SettingsIcon />}
className={buttonClasses}
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
<ContextMenuContent align="start">
<ContextMenuItem onSelect={openRemoveGuidebookAlert}>
{t('context.remove')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{removeAlert()}
</>
)
}
}
const removeAlert = () => {
return (
<Alert
open={alertOpen}
primaryAction={removeGuidebook}
primaryActionText={t('modals.weapon.buttons.remove')}
cancelAction={() => setAlertOpen(false)}
cancelActionText={t('buttons.cancel')}
message={
<Trans i18nKey="modals.guidebooks.messages.remove">
Are you sure you want to remove{' '}
<strong>{{ guidebook: guidebook?.name[locale] }}</strong> from your
team?
</Trans>
}
/>
)
}
const searchModal = () => {
return (
<SearchModal
placeholderText={t('search.placeholders.guidebook')}
fromPosition={position}
object="guidebooks"
open={searchModalOpen}
onOpenChange={handleSearchModalOpenChange}
send={updateObject}
/>
)
}
// Methods: Core element rendering
const imageElement = (
<div className="GuidebookImage" onClick={openSearchModal}>
<img
alt={guidebook?.name[locale]}
className={classNames({
GridImage: true,
Placeholder: imageUrl === '',
})}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl}
/>
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
const unitContent = (
<>
<div className={classes}>
{contextMenu()}
{imageElement}
<h3 className="GuidebookName">{guidebook?.name[locale]}</h3>
</div>
{searchModal()}
</>
)
return unitContent
}
export default GuidebookUnit

View file

@ -0,0 +1,45 @@
.Guidebooks {
#GuidebooksGrid {
display: grid;
gap: $unit-3x;
grid-template-columns: repeat(3, minmax(0, 1fr));
@include breakpoint(tablet) {
gap: $unit-2x;
}
@include breakpoint(phone) {
gap: $unit;
}
.WeaponUnit .WeaponImage {
background: var(--extra-purple-card-bg);
}
.WeaponUnit .WeaponImage .icon svg {
fill: var(--extra-purple-secondary);
}
}
.ExtraGrid.Weapons {
background: var(--extra-purple-bg);
border-radius: $card-corner;
box-sizing: border-box;
display: grid;
grid-template-columns: 1.42fr 3fr;
justify-content: center;
@include breakpoint(tablet) {
left: auto;
max-width: auto;
width: 100%;
}
@include breakpoint(phone) {
display: flex;
gap: $unit-2x;
padding: $unit-2x;
flex-direction: column;
}
}
}

View file

@ -0,0 +1,95 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import Switch from '~components/common/Switch'
import GuidebookUnit from '../GuidebookUnit'
import classNames from 'classnames'
import type { SearchableObject } from '~types'
import './index.scss'
// Props
interface Props {
grid: GuidebookList
editable: boolean
offset: number
removeGuidebook: (id: string) => void
updateObject: (object: SearchableObject, position: number) => void
}
// Constants
const EXTRA_WEAPONS_COUNT = 3
const GuidebooksGrid = ({
grid,
editable,
removeGuidebook,
updateObject,
}: Props) => {
const { t } = useTranslation('common')
const [enabled, setEnabled] = useState(false)
const classes = classNames({
Guidebooks: true,
ContainerItem: true,
Disabled: !enabled,
})
useEffect(() => {
console.log('Grid updated')
if (hasGuidebooks()) setEnabled(true)
}, [grid])
function hasGuidebooks() {
return grid && (grid[0] || grid[1] || grid[2])
}
function onCheckedChange(checked: boolean) {
setEnabled(checked)
}
const enabledElement = (
<ul id="GuidebooksGrid">
{Array.from(Array(EXTRA_WEAPONS_COUNT)).map((x, i) => {
const itemClasses = classNames({
Empty: grid && grid[i] === undefined,
})
return (
<li className={itemClasses} key={`grid_unit_${i}`}>
<GuidebookUnit
editable={editable}
position={i}
guidebook={grid[i]}
removeGuidebook={removeGuidebook}
updateObject={updateObject}
/>
</li>
)
})}
</ul>
)
const guidebookElement = (
<div className={classes}>
<div className="Header">
<h3>{t('sephira_guidebooks')}</h3>
{editable ? (
<Switch
name="Guidebooks"
checked={enabled}
onCheckedChange={onCheckedChange}
/>
) : (
''
)}
</div>
{enabled ? enabledElement : ''}
</div>
)
return editable || (enabled && !editable) ? guidebookElement : <div />
}
export default GuidebooksGrid