diff --git a/components/extra/GuidebookUnit/index.scss b/components/extra/GuidebookUnit/index.scss new file mode 100644 index 00000000..9c6f489d --- /dev/null +++ b/components/extra/GuidebookUnit/index.scss @@ -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; + } +} diff --git a/components/extra/GuidebookUnit/index.tsx b/components/extra/GuidebookUnit/index.tsx new file mode 100644 index 00000000..bb1fc38f --- /dev/null +++ b/components/extra/GuidebookUnit/index.tsx @@ -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 ( + <> + + + } + className={buttonClasses} + onClick={handleButtonClicked} + /> + + + + {t('context.remove')} + + + + {removeAlert()} + > + ) + } + } + + const removeAlert = () => { + return ( + setAlertOpen(false)} + cancelActionText={t('buttons.cancel')} + message={ + + Are you sure you want to remove{' '} + {{ guidebook: guidebook?.name[locale] }} from your + team? + + } + /> + ) + } + + const searchModal = () => { + return ( + + ) + } + + // Methods: Core element rendering + const imageElement = ( + + + {editable ? ( + + + + ) : ( + '' + )} + + ) + + const unitContent = ( + <> + + {contextMenu()} + {imageElement} + {guidebook?.name[locale]} + + {searchModal()} + > + ) + + return unitContent +} + +export default GuidebookUnit diff --git a/components/extra/GuidebooksGrid/index.scss b/components/extra/GuidebooksGrid/index.scss new file mode 100644 index 00000000..6b3e86fc --- /dev/null +++ b/components/extra/GuidebooksGrid/index.scss @@ -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; + } + } +} diff --git a/components/extra/GuidebooksGrid/index.tsx b/components/extra/GuidebooksGrid/index.tsx new file mode 100644 index 00000000..3caad588 --- /dev/null +++ b/components/extra/GuidebooksGrid/index.tsx @@ -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 = ( + + {Array.from(Array(EXTRA_WEAPONS_COUNT)).map((x, i) => { + const itemClasses = classNames({ + Empty: grid && grid[i] === undefined, + }) + + return ( + + + + ) + })} + + ) + + const guidebookElement = ( + + + {t('sephira_guidebooks')} + {editable ? ( + + ) : ( + '' + )} + + {enabled ? enabledElement : ''} + + ) + + return editable || (enabled && !editable) ? guidebookElement : +} + +export default GuidebooksGrid