From db2fe442047f3ae9f953a9c3913e6030c1ec62f6 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 9 Jul 2023 22:47:34 -0700 Subject: [PATCH] Implement MentionEditor and MentionTableField --- .../common/MentionEditor/index.module.scss | 152 ++++++++++++++++++ components/common/MentionEditor/index.tsx | 88 ++++++++++ .../MentionTableField/index.module.scss | 0 components/common/MentionTableField/index.tsx | 40 +++++ .../common/TableField/index.module.scss | 4 + components/filters/FilterModal/index.tsx | 33 +++- 6 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 components/common/MentionEditor/index.module.scss create mode 100644 components/common/MentionEditor/index.tsx create mode 100644 components/common/MentionTableField/index.module.scss create mode 100644 components/common/MentionTableField/index.tsx diff --git a/components/common/MentionEditor/index.module.scss b/components/common/MentionEditor/index.module.scss new file mode 100644 index 00000000..0ab74f54 --- /dev/null +++ b/components/common/MentionEditor/index.module.scss @@ -0,0 +1,152 @@ +.wrapper { + border-radius: $input-corner; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; + + .editor { + -webkit-font-smoothing: antialiased; + box-sizing: border-box; + color: var(--text-primary); + display: block; + flex-grow: 1; + font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, + sans-serif; + font-size: $font-regular; + overflow: scroll; + padding: ($unit * 1.5) $unit-2x; + white-space: pre-wrap; + width: 100%; + + &:focus { + // border: 2px solid $blue; + outline: none; + } + + p { + line-height: 1.5; + } + + p.empty:first-child::before { + color: var(--text-tertiary); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } + + &.bound { + background-color: var(--input-bound-bg); + + &:hover { + background-color: var(--input-bound-bg-hover); + } + } + + .mention { + border-radius: $item-corner-small; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--null-shadow); + background: var(--null-bg); + display: inline-flex; + color: var(--text-primary); + font-weight: $medium; + font-size: 15px; + padding: 1px $unit-half; + margin: $unit-fourth; + transition: all 0.1s ease-out; + + &:hover { + background: var(--null-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--null-shadow-hover); + text-decoration: none; + cursor: pointer; + } + + &[data-element='fire'] { + background: var(--fire-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--fire-shadow); + color: var(--fire-text); + + &:hover { + background: var(--fire-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--fire-shadow-hover); + color: var(--fire-text-hover); + } + } + + &[data-element='water'] { + background: var(--water-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--water-shadow); + color: var(--water-text); + + &:hover { + background: var(--water-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--water-shadow-hover); + color: var(--water-text-hover); + } + } + + &[data-element='earth'] { + background: var(--earth-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--earth-shadow); + color: var(--earth-text); + + &:hover { + background: var(--earth-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--earth-shadow-hover); + color: var(--earth-text-hover); + } + } + + &[data-element='wind'] { + background: var(--wind-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--wind-shadow); + color: var(--wind-text); + + &:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--wind-shadow-hover); + color: var(--wind-text-hover); + } + } + + &[data-element='dark'] { + background: var(--dark-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--dark-shadow); + color: var(--dark-text); + + &:hover { + background: var(--dark-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--dark-shadow-hover); + color: var(--dark-text-hover); + } + } + + &[data-element='light'] { + background: var(--light-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--light-shadow); + color: var(--light-text); + + &:hover { + background: var(--light-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--light-shadow-hover); + color: var(--light-text-hover); + } + } + } + } +} diff --git a/components/common/MentionEditor/index.tsx b/components/common/MentionEditor/index.tsx new file mode 100644 index 00000000..6bb0358e --- /dev/null +++ b/components/common/MentionEditor/index.tsx @@ -0,0 +1,88 @@ +import { ComponentProps, useCallback, useEffect } from 'react' +import { useRouter } from 'next/router' +import type { JSONContent } from '@tiptap/core' +import { useEditor, EditorContent } from '@tiptap/react' +import { useTranslation } from 'next-i18next' + +import Placeholder from '@tiptap/extension-placeholder' +import FilterMention from '~extensions/FilterMention' +import NoNewLine from '~extensions/NoNewLine' +import classNames from 'classnames' + +import { mentionSuggestionOptions } from '~utils/mentionSuggestions' + +import styles from './index.module.scss' +import StarterKit from '@tiptap/starter-kit' + +interface Props extends ComponentProps<'div'> { + bound?: boolean + placeholder?: string + onUpdate?: (content: string[]) => void +} + +const MentionEditor = ({ bound, placeholder, onUpdate, ...props }: Props) => { + const locale = useRouter().locale || 'en' + const { t } = useTranslation('common') + + // Setup: Editor + const editor = useEditor({ + content: '', + editable: true, + editorProps: { + attributes: { + class: classNames({ + [styles.editor]: true, + [styles.bound]: bound, + }), + }, + }, + extensions: [ + StarterKit, + Placeholder.configure({ + emptyEditorClass: styles.empty, + placeholder: placeholder, + }), + NoNewLine, + FilterMention.configure({ + renderLabel({ options, node }) { + return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}` + }, + suggestion: mentionSuggestionOptions, + HTMLAttributes: { + class: classNames({ + [styles.mention]: true, + }), + }, + }), + ], + onFocus: ({ editor }) => { + console.log('Editor reporting that is focused') + }, + onUpdate: ({ editor }) => { + const mentions = parseMentions(editor.getJSON()) + if (onUpdate) onUpdate(mentions) + }, + }) + + return ( +
+ +
+ ) +} + +function parseMentions(data: JSONContent) { + const mentions: string[] = (data.content || []).flatMap(parseMentions) + if (data.type === 'mention') { + const granblueId = data.attrs?.id.granblue_id + mentions.push(granblueId) + } + + return [...new Set(mentions)] +} + +MentionEditor.defaultProps = { + bound: false, +} + +export default MentionEditor diff --git a/components/common/MentionTableField/index.module.scss b/components/common/MentionTableField/index.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/components/common/MentionTableField/index.tsx b/components/common/MentionTableField/index.tsx new file mode 100644 index 00000000..23527574 --- /dev/null +++ b/components/common/MentionTableField/index.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react' +import MentionEditor from '~components/common/MentionEditor' +import TableField from '~components/common/TableField' + +import styles from './index.module.scss' + +interface Props + extends React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + > { + label: string + description?: string + placeholder?: string + onUpdate: (content: string[]) => void +} + +const MentionTableField = ({ + label, + description, + placeholder, + ...props +}: Props) => { + return ( + + + + ) +} + +export default MentionTableField diff --git a/components/common/TableField/index.module.scss b/components/common/TableField/index.module.scss index e48b5328..14894dd8 100644 --- a/components/common/TableField/index.module.scss +++ b/components/common/TableField/index.module.scss @@ -44,6 +44,10 @@ } } + &.mention { + grid-template-columns: 1fr 2fr; + } + .left { align-items: center; display: flex; diff --git a/components/filters/FilterModal/index.tsx b/components/filters/FilterModal/index.tsx index ce73f588..c7ed4f31 100644 --- a/components/filters/FilterModal/index.tsx +++ b/components/filters/FilterModal/index.tsx @@ -18,6 +18,7 @@ import SelectItem from '~components/common/SelectItem' import type { DialogProps } from '@radix-ui/react-dialog' import styles from './index.module.scss' +import MentionTableField from '~components/common/MentionTableField' interface Props extends DialogProps { defaultFilterSet: FilterSet @@ -47,6 +48,8 @@ const FilterModal = (props: Props) => { const [chargeAttackOpen, setChargeAttackOpen] = useState(false) const [fullAutoOpen, setFullAutoOpen] = useState(false) const [autoGuardOpen, setAutoGuardOpen] = useState(false) + const [inclusions, setInclusions] = useState([]) + const [exclusions, setExclusions] = useState([]) const [filterSet, setFilterSet] = useState({}) // Filter states @@ -131,6 +134,9 @@ const FilterModal = (props: Props) => { setCookie('filters', filters, { path: '/' }) } + if (inclusions.length > 0) filters.includes = inclusions.join(',') + if (exclusions.length > 0) filters.excludes = exclusions.join(',') + props.sendAdvancedFilters(filters) openChange() } @@ -384,6 +390,27 @@ const FilterModal = (props: Props) => { /> ) + // Inclusions and exclusions + const inclusionField = ( + setInclusions(value)} + /> + ) + + const exclusionField = ( + setExclusions(value)} + /> + ) + const filterNotice = () => { if (props.persistFilters) return null return ( @@ -404,14 +431,16 @@ const FilterModal = (props: Props) => {
{filterNotice()} + {inclusionField} + {exclusionField} {chargeAttackField()} {fullAutoField()} {autoGuardField()}