Add support for including/excluding items from team filtering (#363)
This PR adds support for including/excluding specific items from team filtering. Users can use the filter modal to only show teams that include specific items, only show teams that _don't_ include specific items, or combine the two to create a very powerful filter.
This commit is contained in:
parent
99c7eb73c1
commit
a4e4328329
35 changed files with 1372 additions and 62 deletions
|
|
@ -11,7 +11,7 @@ import classNames from 'classnames'
|
|||
|
||||
import styles from './index.module.scss'
|
||||
|
||||
type Props = Pick<SuggestionProps, 'items' | 'command'>
|
||||
type Props = Pick<SuggestionProps, 'items' | 'command' | 'query'>
|
||||
|
||||
export type MentionRef = {
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => boolean
|
||||
|
|
@ -113,7 +113,9 @@ export const MentionList = forwardRef<MentionRef, Props>(
|
|||
))
|
||||
) : (
|
||||
<div className={styles.noResult}>
|
||||
{t('search.errors.no_results_generic')}
|
||||
{props.query.length < 3
|
||||
? t('search.errors.type')
|
||||
: t('search.errors.no_results_generic')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -277,8 +277,8 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
|||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogContent
|
||||
className="Account"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
headerRef={headerRef}
|
||||
footerRef={footerRef}
|
||||
onOpenAutoFocus={(event: Event) => {}}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ const LoginModal = (props: Props) => {
|
|||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogContent
|
||||
className="login"
|
||||
footerref={footerRef}
|
||||
footerRef={footerRef}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
onOpenAutoFocus={onOpenAutoFocus}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ const SignupModal = (props: Props) => {
|
|||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogContent
|
||||
className="signup"
|
||||
footerref={footerRef}
|
||||
footerRef={footerRef}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
onOpenAutoFocus={onOpenAutoFocus}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const CharacterConflictModal = (props: Props) => {
|
|||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogContent
|
||||
className="conflict"
|
||||
footerref={footerRef}
|
||||
footerRef={footerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={close}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -354,8 +354,8 @@ const CharacterModal = ({
|
|||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="character"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
headerRef={headerRef}
|
||||
footerRef={footerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={() => {}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ interface Props
|
|||
HTMLDivElement
|
||||
> {
|
||||
wrapperClassName?: string
|
||||
headerref?: React.RefObject<HTMLDivElement>
|
||||
footerref?: React.RefObject<HTMLDivElement>
|
||||
headerRef?: React.RefObject<HTMLDivElement>
|
||||
footerRef?: React.RefObject<HTMLDivElement>
|
||||
scrollable?: boolean
|
||||
onEscapeKeyDown: (event: KeyboardEvent) => void
|
||||
onOpenAutoFocus: (event: Event) => void
|
||||
}
|
||||
|
||||
const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
||||
{ scrollable, children, ...props },
|
||||
{ scrollable, wrapperClassName, headerRef, footerRef, children, ...props },
|
||||
forwardedRef
|
||||
) {
|
||||
// Classes
|
||||
|
|
@ -37,12 +37,12 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
|||
const scrollHeight = event.currentTarget.scrollHeight
|
||||
const clientHeight = event.currentTarget.clientHeight
|
||||
|
||||
if (props.headerref && props.headerref.current)
|
||||
manipulateHeaderShadow(props.headerref.current, scrollTop)
|
||||
if (headerRef && headerRef.current)
|
||||
manipulateHeaderShadow(headerRef.current, scrollTop)
|
||||
|
||||
if (props.footerref && props.footerref.current)
|
||||
if (footerRef && footerRef.current)
|
||||
manipulateFooterShadow(
|
||||
props.footerref.current,
|
||||
footerRef.current,
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
clientHeight
|
||||
|
|
@ -94,7 +94,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
|||
const calculateFooterShadow = debounce(() => {
|
||||
const boxShadowBase = '0 -2px 8px'
|
||||
const scrollable = document.querySelector(`.${styles.scrollable}`)
|
||||
const footer = props.footerref
|
||||
const footer = footerRef
|
||||
|
||||
if (footer && footer.current) {
|
||||
if (scrollable && scrollable.clientHeight >= scrollable.scrollHeight) {
|
||||
|
|
@ -133,9 +133,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
|||
{
|
||||
[styles.dialog]: true,
|
||||
},
|
||||
props.wrapperClassName
|
||||
?.split(' ')
|
||||
.map((className) => styles[className])
|
||||
wrapperClassName?.split(' ').map((className) => styles[className])
|
||||
)}
|
||||
>
|
||||
<DialogPrimitive.Content
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
|
||||
position: sticky;
|
||||
transition: box-shadow 0.1s ease-out, border-top 0.1s ease-out;
|
||||
z-index: 10;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
|
|
|
|||
0
components/common/MentionTableField/index.module.scss
Normal file
0
components/common/MentionTableField/index.module.scss
Normal file
45
components/common/MentionTableField/index.tsx
Normal file
45
components/common/MentionTableField/index.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import TableField from '~components/common/TableField'
|
||||
import MentionTypeahead from '../MentionTypeahead'
|
||||
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
inclusions: MentionItem[]
|
||||
exclusions: MentionItem[]
|
||||
onUpdate: (content: MentionItem[]) => void
|
||||
}
|
||||
|
||||
const MentionTableField = ({
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
inclusions,
|
||||
exclusions,
|
||||
...props
|
||||
}: Props) => {
|
||||
return (
|
||||
<TableField
|
||||
{...props}
|
||||
name={props.name || ''}
|
||||
description={description}
|
||||
className="mention"
|
||||
label={label}
|
||||
>
|
||||
<MentionTypeahead
|
||||
label={label}
|
||||
description={description}
|
||||
placeholder={placeholder}
|
||||
inclusions={inclusions}
|
||||
exclusions={exclusions}
|
||||
onUpdate={props.onUpdate}
|
||||
/>
|
||||
</TableField>
|
||||
)
|
||||
}
|
||||
|
||||
export default MentionTableField
|
||||
358
components/common/MentionTypeahead/index.module.scss
Normal file
358
components/common/MentionTypeahead/index.module.scss
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
.menu {
|
||||
background: #fff;
|
||||
border-radius: $item-corner;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
overflow: scroll;
|
||||
padding: $unit-half;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
width: 200px;
|
||||
z-index: 999;
|
||||
|
||||
:global(.dropdown-item.disabled) {
|
||||
align-items: center;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
font-size: $font-small;
|
||||
min-height: $unit-5x;
|
||||
padding-left: $unit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
[aria-selected='true'] .item {
|
||||
background: var(--menu-bg-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $item-corner-small;
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
margin: 0;
|
||||
padding: $unit-half $unit;
|
||||
text-align: left;
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: var(--menu-bg-item-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.job {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $unit-4x;
|
||||
height: $unit-4x;
|
||||
|
||||
img {
|
||||
width: $unit-3x;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $item-corner-small;
|
||||
width: $unit-4x;
|
||||
height: $unit-4x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typeahead {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-size: $font-regular;
|
||||
white-space: pre-wrap;
|
||||
max-width: 240px;
|
||||
width: 100%;
|
||||
|
||||
:global(.rbt-input) {
|
||||
background-color: var(--input-bound-bg);
|
||||
border-radius: $input-corner;
|
||||
color: var(--text-primary);
|
||||
padding: ($unit * 1.5) $unit-2x;
|
||||
min-height: 26px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--input-bound-bg-hover);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:global(.focus) {
|
||||
outline: 2px solid #275dc5;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.rbt-input-wrapper) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 30px;
|
||||
margin-bottom: -4px;
|
||||
margin-top: -1px;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.rbt-input-hint) {
|
||||
color: var(--text-tertiary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token {
|
||||
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);
|
||||
color: var(--text-primary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: $medium;
|
||||
font-size: 15px;
|
||||
padding: 1px $unit-half 1px $unit;
|
||||
margin: $unit-fourth;
|
||||
transition: all 0.1s ease-out;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.rbt-token-label) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
background: none;
|
||||
border: none;
|
||||
font-weight: $bold;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.visually-hidden) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
$outline: 2px solid var(--transparent-stroke);
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
&[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);
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--fire-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--fire-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--fire-text);
|
||||
box-shadow: none;
|
||||
color: var(--fire-bg);
|
||||
outline: $outline;
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--fire-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[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);
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--water-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--water-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--water-text);
|
||||
box-shadow: none;
|
||||
color: var(--water-bg);
|
||||
outline: $outline;
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--water-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[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);
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--earth-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--earth-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--earth-text);
|
||||
box-shadow: none;
|
||||
color: var(--earth-bg);
|
||||
outline: $outline;
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--earth-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[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);
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--wind-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--wind-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--wind-text);
|
||||
box-shadow: none;
|
||||
color: var(--wind-bg);
|
||||
outline: $outline;
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--wind-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[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);
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--dark-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--dark-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--dark-text);
|
||||
box-shadow: none;
|
||||
color: var(--dark-bg);
|
||||
outline: $outline;
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--dark-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[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);
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--light-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--light-text);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||
0 1px 0px var(--light-shadow-hover);
|
||||
color: var(--light-bg);
|
||||
outline: $outline;
|
||||
|
||||
:global(.rbt-token-remove-button) {
|
||||
color: var(--light-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
components/common/MentionTypeahead/index.tsx
Normal file
186
components/common/MentionTypeahead/index.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { useState } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import type {
|
||||
Option,
|
||||
RenderTokenProps,
|
||||
} from 'react-bootstrap-typeahead/types/types'
|
||||
|
||||
import {
|
||||
AsyncTypeahead,
|
||||
Menu,
|
||||
MenuItem,
|
||||
RenderMenuProps,
|
||||
Token,
|
||||
} from 'react-bootstrap-typeahead'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { numberToElement } from '~utils/elements'
|
||||
|
||||
import styles from './index.module.scss'
|
||||
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
inclusions: MentionItem[]
|
||||
exclusions: MentionItem[]
|
||||
onUpdate: (content: MentionItem[]) => void
|
||||
}
|
||||
|
||||
interface RawSearchResponse {
|
||||
searchable_type: string
|
||||
granblue_id: string
|
||||
name_en: string
|
||||
name_jp: string
|
||||
element: number
|
||||
}
|
||||
|
||||
const MentionTypeahead = ({
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
inclusions,
|
||||
exclusions,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
const locale = getCookie('NEXT_LOCALE')
|
||||
? (getCookie('NEXT_LOCALE') as string)
|
||||
: 'en'
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [options, setOptions] = useState<Option[]>([])
|
||||
|
||||
async function handleSearch(query: string) {
|
||||
setIsLoading(true)
|
||||
|
||||
const exclude = transformIntoString([...inclusions, ...exclusions])
|
||||
|
||||
const response = await api.searchAll(query, exclude, locale)
|
||||
const results = response.data.results
|
||||
|
||||
setIsLoading(false)
|
||||
setOptions(mapResults(results))
|
||||
}
|
||||
|
||||
function transformIntoMentionItem(object: RawSearchResponse) {
|
||||
const result: MentionItem = {
|
||||
name: {
|
||||
en: object.name_en,
|
||||
ja: object.name_jp,
|
||||
},
|
||||
type: object.searchable_type.toLowerCase(),
|
||||
granblue_id: object.granblue_id,
|
||||
element: numberToElement(object.element),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function transformIntoString(list: MentionItem[]) {
|
||||
return list.map((item) => item.granblue_id)
|
||||
}
|
||||
|
||||
function mapResults(results: RawSearchResponse[]) {
|
||||
return results
|
||||
.map((rawObject: RawSearchResponse) => {
|
||||
const object = transformIntoMentionItem(rawObject)
|
||||
return {
|
||||
granblue_id: object.granblue_id,
|
||||
element: object.element,
|
||||
type: object.type,
|
||||
name: {
|
||||
en: object.name.en,
|
||||
ja: object.name.ja,
|
||||
},
|
||||
}
|
||||
})
|
||||
.slice(0, 5)
|
||||
}
|
||||
|
||||
function renderMenu(results: Option[], menuProps: RenderMenuProps) {
|
||||
return (
|
||||
<Menu
|
||||
{...menuProps}
|
||||
className={styles.menu}
|
||||
emptyLabel={t('modals.filters.prompts.not_found')}
|
||||
>
|
||||
{results.map((option, index) => (
|
||||
<MenuItem key={index} option={option} position={index}>
|
||||
{renderMenuItemChild(option)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
function renderMenuItemChild(option: Option) {
|
||||
const item = option as MentionItem
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles[item.type]}>
|
||||
<img
|
||||
alt={item.name[locale]}
|
||||
src={
|
||||
item.type === 'character'
|
||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/${item.type}-square/${item.granblue_id}_01.jpg`
|
||||
: item.type === 'job'
|
||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${item.granblue_id}.png`
|
||||
: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/${item.type}-square/${item.granblue_id}.jpg`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span>{item.name[locale]}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderToken(option: Option, props: RenderTokenProps) {
|
||||
const item = option as MentionItem
|
||||
const { labelKey, ...tokenProps } = props
|
||||
return (
|
||||
<Token
|
||||
{...tokenProps}
|
||||
className={styles.token}
|
||||
data-element={item.element.slug}
|
||||
data-type={item.type}
|
||||
option={option}
|
||||
>
|
||||
{item.name[locale]}
|
||||
</Token>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncTypeahead
|
||||
multiple
|
||||
className={styles.typeahead}
|
||||
id={label}
|
||||
align="left"
|
||||
isLoading={isLoading}
|
||||
labelKey={(option) => (option as MentionItem).name[locale]}
|
||||
defaultSelected={inclusions}
|
||||
filterBy={() => true}
|
||||
minLength={3}
|
||||
onSearch={handleSearch}
|
||||
options={options}
|
||||
useCache={false}
|
||||
placeholder={placeholder}
|
||||
positionFixed={true}
|
||||
promptText={t('modals.filters.prompts.type')}
|
||||
searchText={t('modals.filters.prompts.searching')}
|
||||
renderMenu={renderMenu}
|
||||
renderMenuItemChildren={renderMenuItemChild}
|
||||
renderToken={renderToken}
|
||||
highlightOnlyResult={false}
|
||||
onChange={(selected) => props.onUpdate(selected as MentionItem[])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default MentionTypeahead
|
||||
|
|
@ -44,6 +44,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.mention {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
h3 {
|
||||
font-size: $font-medium;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-small;
|
||||
}
|
||||
|
||||
&.border {
|
||||
border-top: 1px solid var(--text-tertiary);
|
||||
padding-top: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
background-color: var(--notice-bg);
|
||||
border-radius: $input-corner;
|
||||
|
|
@ -34,11 +50,16 @@
|
|||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-4x;
|
||||
gap: $unit-4x;
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-4x;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
gap: $unit-4x;
|
||||
margin-bottom: $unit * 24;
|
||||
@include breakpoint(phone) {
|
||||
gap: $unit-4x;
|
||||
margin-bottom: $unit * 24;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ 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'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props extends DialogProps {
|
||||
defaultFilterSet: FilterSet
|
||||
|
|
@ -47,6 +49,8 @@ const FilterModal = (props: Props) => {
|
|||
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
|
||||
const [fullAutoOpen, setFullAutoOpen] = useState(false)
|
||||
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
|
||||
const [inclusions, setInclusions] = useState<MentionItem[]>([])
|
||||
const [exclusions, setExclusions] = useState<MentionItem[]>([])
|
||||
const [filterSet, setFilterSet] = useState<FilterSet>({})
|
||||
|
||||
// Filter states
|
||||
|
|
@ -82,11 +86,17 @@ const FilterModal = (props: Props) => {
|
|||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
if (props.open !== undefined) setOpen(props.open)
|
||||
if (props.open !== undefined) {
|
||||
setOpen(props.open)
|
||||
|
||||
// When should we reset the filter state?
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setFilterSet(props.filterSet)
|
||||
setInclusions(props.filterSet.includes || [])
|
||||
setExclusions(props.filterSet.excludes || [])
|
||||
}, [props.filterSet])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -127,6 +137,9 @@ const FilterModal = (props: Props) => {
|
|||
if (maxButtonsCount) filters.button_count = maxButtonsCount
|
||||
if (maxTurnsCount) filters.turn_count = maxTurnsCount
|
||||
|
||||
if (inclusions.length > 0) filters.includes = inclusions
|
||||
if (exclusions.length > 0) filters.excludes = exclusions
|
||||
|
||||
if (props.persistFilters) {
|
||||
setCookie('filters', filters, { path: '/' })
|
||||
}
|
||||
|
|
@ -384,6 +397,37 @@ const FilterModal = (props: Props) => {
|
|||
/>
|
||||
)
|
||||
|
||||
// Inclusions and exclusions
|
||||
function storeInclusions(value: MentionItem[]) {
|
||||
setInclusions(value)
|
||||
}
|
||||
|
||||
function storeExclusions(value: MentionItem[]) {
|
||||
setExclusions(value)
|
||||
}
|
||||
|
||||
const inclusionField = (
|
||||
<MentionTableField
|
||||
name="inclusion"
|
||||
inclusions={inclusions}
|
||||
exclusions={exclusions}
|
||||
placeholder={t('modals.filters.placeholders.inclusion')}
|
||||
label={t('modals.filters.labels.inclusion')}
|
||||
onUpdate={storeInclusions}
|
||||
/>
|
||||
)
|
||||
|
||||
const exclusionField = (
|
||||
<MentionTableField
|
||||
name="exclusion"
|
||||
inclusions={exclusions}
|
||||
exclusions={inclusions}
|
||||
placeholder={t('modals.filters.placeholders.exclusion')}
|
||||
label={t('modals.filters.labels.exclusion')}
|
||||
onUpdate={storeExclusions}
|
||||
/>
|
||||
)
|
||||
|
||||
const filterNotice = () => {
|
||||
if (props.persistFilters) return null
|
||||
return (
|
||||
|
|
@ -404,25 +448,39 @@ const FilterModal = (props: Props) => {
|
|||
<DialogContent
|
||||
className="filter"
|
||||
wrapperClassName="filter"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
headerRef={headerRef}
|
||||
footerRef={footerRef}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
onOpenAutoFocus={onOpenAutoFocus}
|
||||
>
|
||||
<DialogHeader title={t('modals.filters.title')} />
|
||||
<div className={styles.fields}>
|
||||
{filterNotice()}
|
||||
{chargeAttackField()}
|
||||
{fullAutoField()}
|
||||
{autoGuardField()}
|
||||
{/* {maxButtonsField()} */}
|
||||
{/* {maxTurnsField()} */}
|
||||
{minCharactersField()}
|
||||
{minSummonsField()}
|
||||
{minWeaponsField()}
|
||||
{nameQualityField()}
|
||||
{userQualityField()}
|
||||
{originalOnlyField()}
|
||||
<section>
|
||||
<div className={styles.header}>
|
||||
<h3>{t('modals.filters.headers.items.name')}</h3>
|
||||
<p>{t('modals.filters.headers.items.description')}</p>
|
||||
</div>
|
||||
{inclusionField}
|
||||
{exclusionField}
|
||||
</section>
|
||||
<section>
|
||||
<div className={styles.header}>
|
||||
<h3>{t('modals.filters.headers.details.name')}</h3>
|
||||
<p>{t('modals.filters.headers.details.description')}</p>
|
||||
</div>
|
||||
{chargeAttackField()}
|
||||
{fullAutoField()}
|
||||
{autoGuardField()}
|
||||
{/* {maxButtonsField()} */}
|
||||
{/* {maxTurnsField()} */}
|
||||
{minCharactersField()}
|
||||
{minSummonsField()}
|
||||
{minWeaponsField()}
|
||||
{nameQualityField()}
|
||||
{userQualityField()}
|
||||
{originalOnlyField()}
|
||||
</section>
|
||||
</div>
|
||||
<DialogFooter
|
||||
ref={footerRef}
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ const SearchModal = (props: Props) => {
|
|||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="search"
|
||||
headerref={headerRef}
|
||||
headerRef={headerRef}
|
||||
scrollable={false}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
onOpenAutoFocus={onOpenAutoFocus}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const WeaponConflictModal = (props: Props) => {
|
|||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogContent
|
||||
className="conflict"
|
||||
footerref={footerRef}
|
||||
footerRef={footerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={close}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -469,8 +469,8 @@ const WeaponModal = ({
|
|||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Weapon"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
headerRef={headerRef}
|
||||
footerRef={footerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
>
|
||||
|
|
|
|||
80
extensions/CustomSuggestion/findSuggestionMatch.tsx
Normal file
80
extensions/CustomSuggestion/findSuggestionMatch.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { escapeForRegEx, Range } from '@tiptap/core'
|
||||
import { ResolvedPos } from '@tiptap/pm/model'
|
||||
|
||||
export interface Trigger {
|
||||
char: string
|
||||
allowSpaces: boolean
|
||||
allowedPrefixes: string[] | null
|
||||
startOfLine: boolean
|
||||
$position: ResolvedPos
|
||||
}
|
||||
|
||||
export type SuggestionMatch = {
|
||||
range: Range
|
||||
query: string
|
||||
text: string
|
||||
} | null
|
||||
|
||||
export function findSuggestionMatch(config: Trigger): SuggestionMatch {
|
||||
const { char, allowSpaces, allowedPrefixes, startOfLine, $position } = config
|
||||
|
||||
const escapedChar = escapeForRegEx(char)
|
||||
const suffix = new RegExp(`\\s${escapedChar}$`)
|
||||
const prefix = startOfLine ? '^' : ''
|
||||
// const regexp = allowSpaces
|
||||
// ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${escapedChar}|$)`, 'gm')
|
||||
// : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${escapedChar}]*`, 'gm')
|
||||
const regexp = new RegExp(`^(.*)$`, 'gm')
|
||||
|
||||
const text = $position.nodeBefore?.isText && $position.nodeBefore.text
|
||||
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
const textFrom = $position.pos - text.length
|
||||
const match = Array.from(text.matchAll(regexp)).pop()
|
||||
|
||||
if (!match || match.input === undefined || match.index === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
// JavaScript doesn't have lookbehinds. This hacks a check that first character
|
||||
// is a space or the start of the line
|
||||
const matchPrefix = match.input.slice(
|
||||
Math.max(0, match.index - 1),
|
||||
match.index
|
||||
)
|
||||
const matchPrefixIsAllowed = new RegExp(
|
||||
`^[${allowedPrefixes?.join('')}\0]?$`
|
||||
).test(matchPrefix)
|
||||
|
||||
if (allowedPrefixes !== null && !matchPrefixIsAllowed) {
|
||||
return null
|
||||
}
|
||||
|
||||
// The absolute position of the match in the document
|
||||
const from = textFrom + match.index
|
||||
let to = from + match[0].length
|
||||
|
||||
// Edge case handling; if spaces are allowed and we're directly in between
|
||||
// two triggers
|
||||
if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
|
||||
match[0] += ' '
|
||||
to += 1
|
||||
}
|
||||
|
||||
// If the $position is located within the matched substring, return that range
|
||||
if (from < $position.pos && to >= $position.pos) {
|
||||
return {
|
||||
range: {
|
||||
from,
|
||||
to,
|
||||
},
|
||||
query: match[0],
|
||||
text: match[0],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
291
extensions/CustomSuggestion/index.tsx
Normal file
291
extensions/CustomSuggestion/index.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import { Editor, Range } from '@tiptap/core'
|
||||
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { Decoration, DecorationSet, EditorView } from '@tiptap/pm/view'
|
||||
|
||||
import { findSuggestionMatch } from './findSuggestionMatch'
|
||||
|
||||
export interface SuggestionOptions<I = any> {
|
||||
pluginKey?: PluginKey
|
||||
editor: Editor
|
||||
char?: string
|
||||
allowSpaces?: boolean
|
||||
allowedPrefixes?: string[] | null
|
||||
startOfLine?: boolean
|
||||
decorationTag?: string
|
||||
decorationClass?: string
|
||||
command?: (props: { editor: Editor; range: Range; props: I }) => void
|
||||
items?: (props: { query: string; editor: Editor }) => I[] | Promise<I[]>
|
||||
render?: () => {
|
||||
onBeforeStart?: (props: SuggestionProps<I>) => void
|
||||
onStart?: (props: SuggestionProps<I>) => void
|
||||
onBeforeUpdate?: (props: SuggestionProps<I>) => void
|
||||
onUpdate?: (props: SuggestionProps<I>) => void
|
||||
onExit?: (props: SuggestionProps<I>) => void
|
||||
onFocus?: (props: SuggestionProps<I>) => void
|
||||
onKeyDown?: (props: SuggestionKeyDownProps) => boolean
|
||||
}
|
||||
allow?: (props: {
|
||||
editor: Editor
|
||||
state: EditorState
|
||||
range: Range
|
||||
}) => boolean
|
||||
}
|
||||
|
||||
export interface SuggestionProps<I = any> {
|
||||
editor: Editor
|
||||
range: Range
|
||||
query: string
|
||||
text: string
|
||||
items: I[]
|
||||
command: (props: I) => void
|
||||
decorationNode: Element | null
|
||||
clientRect?: (() => DOMRect) | null
|
||||
}
|
||||
|
||||
export interface SuggestionKeyDownProps {
|
||||
view: EditorView
|
||||
event: KeyboardEvent
|
||||
range: Range
|
||||
}
|
||||
|
||||
export const SuggestionPluginKey = new PluginKey('suggestion')
|
||||
|
||||
export function CustomSuggestion<I = any>({
|
||||
pluginKey = SuggestionPluginKey,
|
||||
editor,
|
||||
char = '@',
|
||||
allowSpaces = true,
|
||||
allowedPrefixes = [' '],
|
||||
startOfLine = false,
|
||||
decorationTag = 'span',
|
||||
decorationClass = 'suggestion',
|
||||
command = () => null,
|
||||
items = () => [],
|
||||
render = () => ({}),
|
||||
allow = () => true,
|
||||
}: SuggestionOptions<I>) {
|
||||
let props: SuggestionProps<I> | undefined
|
||||
const renderer = render?.()
|
||||
|
||||
const plugin: Plugin<any> = new Plugin({
|
||||
key: pluginKey,
|
||||
|
||||
view() {
|
||||
return {
|
||||
update: async (view, prevState) => {
|
||||
// This should be this.key?
|
||||
const prev = pluginKey.getState(prevState)
|
||||
const next = pluginKey.getState(view.state)
|
||||
|
||||
// See how the state changed
|
||||
const moved =
|
||||
prev.active && next.active && prev.range.from !== next.range.from
|
||||
const started = !prev.active && next.active
|
||||
const stopped = prev.active && !next.active
|
||||
const changed = !started && !stopped && prev.query !== next.query
|
||||
const handleStart = started || moved
|
||||
const handleChange = changed && !moved
|
||||
const handleExit = stopped || moved
|
||||
const handleFocused = view.hasFocus()
|
||||
console.log(handleFocused)
|
||||
|
||||
// Cancel when suggestion isn't active
|
||||
if (!handleStart && !handleChange && !handleExit && !handleFocused) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = handleExit && !handleStart ? prev : next
|
||||
const decorationNode = view.dom.querySelector(
|
||||
`[data-decoration-id="${state.decorationId}"]`
|
||||
)
|
||||
|
||||
props = {
|
||||
editor,
|
||||
range: state.range,
|
||||
query: state.query,
|
||||
text: state.text,
|
||||
items: [],
|
||||
command: (commandProps) => {
|
||||
command({
|
||||
editor,
|
||||
range: state.range,
|
||||
props: commandProps,
|
||||
})
|
||||
},
|
||||
decorationNode,
|
||||
// virtual node for popper.js or tippy.js
|
||||
// this can be used for building popups without a DOM node
|
||||
clientRect: decorationNode
|
||||
? () => {
|
||||
// because of `items` can be asynchrounous we’ll search for the current decoration node
|
||||
const { decorationId } = pluginKey.getState(editor.state) // eslint-disable-line
|
||||
const currentDecorationNode = view.dom.querySelector(
|
||||
`[data-decoration-id="${decorationId}"]`
|
||||
)
|
||||
|
||||
// @ts-ignore-error
|
||||
return currentDecorationNode.getBoundingClientRect()
|
||||
}
|
||||
: null,
|
||||
}
|
||||
|
||||
if (handleFocused) {
|
||||
console.log('Handling focus')
|
||||
}
|
||||
|
||||
if (handleStart) {
|
||||
renderer?.onBeforeStart?.(props)
|
||||
}
|
||||
|
||||
if (handleChange) {
|
||||
renderer?.onBeforeUpdate?.(props)
|
||||
}
|
||||
|
||||
if (handleChange || handleStart) {
|
||||
props.items = await items({
|
||||
editor,
|
||||
query: state.query,
|
||||
})
|
||||
}
|
||||
|
||||
if (handleExit) {
|
||||
renderer?.onExit?.(props)
|
||||
}
|
||||
|
||||
if (handleChange) {
|
||||
renderer?.onUpdate?.(props)
|
||||
}
|
||||
|
||||
if (handleStart) {
|
||||
renderer?.onStart?.(props)
|
||||
}
|
||||
},
|
||||
|
||||
destroy: () => {
|
||||
if (!props) {
|
||||
return
|
||||
}
|
||||
|
||||
renderer?.onExit?.(props)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
state: {
|
||||
// Initialize the plugin's internal state.
|
||||
init() {
|
||||
const state: {
|
||||
active: boolean
|
||||
range: Range
|
||||
query: null | string
|
||||
text: null | string
|
||||
composing: boolean
|
||||
decorationId?: string | null
|
||||
} = {
|
||||
active: false,
|
||||
range: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
query: null,
|
||||
text: null,
|
||||
composing: false,
|
||||
}
|
||||
|
||||
return state
|
||||
},
|
||||
|
||||
// Apply changes to the plugin state from a view transaction.
|
||||
apply(transaction, prev, oldState, state) {
|
||||
const { isEditable } = editor
|
||||
const { composing } = editor.view
|
||||
const { selection } = transaction
|
||||
const { empty, from } = selection
|
||||
const next = { ...prev }
|
||||
|
||||
next.composing = composing
|
||||
|
||||
// We can only be suggesting if the view is editable, and:
|
||||
// * there is no selection, or
|
||||
// * a composition is active (see: https://github.com/ueberdosis/tiptap/issues/1449)
|
||||
if (isEditable && (empty || editor.view.composing)) {
|
||||
// Reset active state if we just left the previous suggestion range
|
||||
if (
|
||||
(from < prev.range.from || from > prev.range.to) &&
|
||||
!composing &&
|
||||
!prev.composing
|
||||
) {
|
||||
next.active = false
|
||||
}
|
||||
|
||||
// Try to match against where our cursor currently is
|
||||
const match = findSuggestionMatch({
|
||||
char,
|
||||
allowSpaces,
|
||||
allowedPrefixes,
|
||||
startOfLine,
|
||||
$position: selection.$from,
|
||||
})
|
||||
const decorationId = `id_${Math.floor(Math.random() * 0xffffffff)}`
|
||||
|
||||
// If we found a match, update the current state to show it
|
||||
if (match && allow({ editor, state, range: match.range })) {
|
||||
next.active = true
|
||||
next.decorationId = prev.decorationId
|
||||
? prev.decorationId
|
||||
: decorationId
|
||||
next.range = match.range
|
||||
next.query = match.query
|
||||
next.text = match.text
|
||||
} else {
|
||||
next.active = false
|
||||
}
|
||||
} else {
|
||||
next.active = false
|
||||
}
|
||||
|
||||
// Make sure to empty the range if suggestion is inactive
|
||||
if (!next.active) {
|
||||
next.decorationId = null
|
||||
next.range = { from: 0, to: 0 }
|
||||
next.query = null
|
||||
next.text = null
|
||||
}
|
||||
|
||||
return next
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
// Call the keydown hook if suggestion is active.
|
||||
handleKeyDown(view, event) {
|
||||
const { active, range } = plugin.getState(view.state)
|
||||
|
||||
if (!active) {
|
||||
return false
|
||||
}
|
||||
|
||||
return renderer?.onKeyDown?.({ view, event, range }) || false
|
||||
},
|
||||
|
||||
// Setup decorator on the currently active suggestion.
|
||||
decorations(state) {
|
||||
const { active, range, decorationId } = plugin.getState(state)
|
||||
|
||||
if (!active) {
|
||||
return null
|
||||
}
|
||||
|
||||
return DecorationSet.create(state.doc, [
|
||||
Decoration.inline(range.from, range.to, {
|
||||
nodeName: decorationTag,
|
||||
class: decorationClass,
|
||||
'data-decoration-id': decorationId,
|
||||
}),
|
||||
])
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return plugin
|
||||
}
|
||||
26
extensions/NoNewLine/index.tsx
Normal file
26
extensions/NoNewLine/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
|
||||
const NoNewLine = Extension.create({
|
||||
name: 'no_new_line',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('eventHandler'),
|
||||
props: {
|
||||
handleKeyDown: (view, event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
console.log('enter pressed')
|
||||
return true
|
||||
}
|
||||
},
|
||||
// … and many, many more.
|
||||
// Here is the full list: https://prosemirror.net/docs/ref/#view.EditorProps
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
export default NoNewLine
|
||||
137
package-lock.json
generated
137
package-lock.json
generated
|
|
@ -31,6 +31,7 @@
|
|||
"@tiptap/react": "^2.0.3",
|
||||
"@tiptap/starter-kit": "^2.0.3",
|
||||
"@tiptap/suggestion": "2.0.0-beta.91",
|
||||
"@types/react-bootstrap-typeahead": "^5.1.9",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
|
|
@ -53,6 +54,7 @@
|
|||
"next-usequerystate": "^1.7.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap-typeahead": "^6.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^11.15.5",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
|
|
@ -4486,6 +4488,17 @@
|
|||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/hooks": {
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz",
|
||||
"integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rushstack/eslint-patch": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz",
|
||||
|
|
@ -7890,6 +7903,14 @@
|
|||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-bootstrap-typeahead": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-bootstrap-typeahead/-/react-bootstrap-typeahead-5.1.9.tgz",
|
||||
"integrity": "sha512-i9/oXJb9EOtotvpzlS8+06HN1s/YCprQKMCJxToOZoTLogyi5dR4hp3PqLCPUL3ciB17CS/4zWE9qfaCF1vWLA==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "17.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz",
|
||||
|
|
@ -7989,6 +8010,11 @@
|
|||
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
||||
"integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA=="
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
||||
|
|
@ -10368,6 +10394,11 @@
|
|||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
|
@ -11076,7 +11107,6 @@
|
|||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
@ -11206,6 +11236,15 @@
|
|||
"utila": "~0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
|
|
@ -18645,6 +18684,29 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-bootstrap-typeahead": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.2.3.tgz",
|
||||
"integrity": "sha512-Ge2au2WxR8CWsAH3GbKsaJpIEV2OMKum2Ov7/kuVMBlHNKwsJc2ULJIjk3yZMoTvvfOzOnYDScaWrQwzPc5c/A==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.6",
|
||||
"@popperjs/core": "^2.10.2",
|
||||
"@restart/hooks": "^0.4.0",
|
||||
"classnames": "^2.2.0",
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"invariant": "^2.2.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"prop-types": "^15.5.8",
|
||||
"react-overlays": "^5.2.0",
|
||||
"react-popper": "^2.2.5",
|
||||
"scroll-into-view-if-needed": "^2.2.20",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
|
|
@ -18727,6 +18789,11 @@
|
|||
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "11.18.6",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
|
||||
|
|
@ -18774,6 +18841,11 @@
|
|||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/react-linkify": {
|
||||
"version": "1.0.0-alpha",
|
||||
"resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz",
|
||||
|
|
@ -18792,6 +18864,39 @@
|
|||
"react-dom": ">=16.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-overlays": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz",
|
||||
"integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.8",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@restart/hooks": "^0.4.7",
|
||||
"@types/warning": "^3.0.0",
|
||||
"dom-helpers": "^5.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0",
|
||||
"react-dom": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-popper": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
|
||||
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
|
||||
"dependencies": {
|
||||
"react-fast-compare": "^3.0.1",
|
||||
"warning": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.0.0",
|
||||
"react": "^16.8.0 || ^17 || ^18",
|
||||
"react-dom": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||
|
|
@ -19746,6 +19851,14 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "2.2.31",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": "^1.0.20"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
|
|
@ -21243,6 +21356,20 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/uncontrollable": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
|
||||
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/react": ">=16.9.11",
|
||||
"invariant": "^2.2.4",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
||||
|
|
@ -21761,6 +21888,14 @@
|
|||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
"@tiptap/react": "^2.0.3",
|
||||
"@tiptap/starter-kit": "^2.0.3",
|
||||
"@tiptap/suggestion": "2.0.0-beta.91",
|
||||
"@types/react-bootstrap-typeahead": "^5.1.9",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
"next-usequerystate": "^1.7.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap-typeahead": "^6.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^11.15.5",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
|
|
|
|||
|
|
@ -158,9 +158,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
{!appState.version ? (
|
||||
serverUnavailable()
|
||||
) : (
|
||||
<main>
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
<Component {...pageProps} />
|
||||
)}
|
||||
</Layout>
|
||||
<Viewport className="ToastViewport" />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { appState } from '~utils/appState'
|
|||
import { defaultFilterset } from '~utils/defaultFilters'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
|
||||
|
||||
import ErrorSection from '~components/ErrorSection'
|
||||
import GridRep from '~components/GridRep'
|
||||
|
|
@ -157,7 +158,7 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
raid: raid === 'all' ? undefined : raid,
|
||||
recency: recency !== -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
...advancedFilters,
|
||||
...convertAdvancedFilters(advancedFilters),
|
||||
}
|
||||
|
||||
Object.keys(filters).forEach(
|
||||
|
|
@ -393,7 +394,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raidGroups)
|
||||
const params = {
|
||||
params: { ...filters, ...advancedFilters },
|
||||
params: { ...filters, ...convertAdvancedFilters(advancedFilters) },
|
||||
}
|
||||
|
||||
// Set up empty variables
|
||||
|
|
|
|||
|
|
@ -252,6 +252,16 @@
|
|||
},
|
||||
"filters": {
|
||||
"title": "Advanced filters",
|
||||
"headers": {
|
||||
"items": {
|
||||
"name": "Filter items",
|
||||
"description": "Show or hide teams that have specific characters, weapons, or summons"
|
||||
},
|
||||
"details": {
|
||||
"name": "Filter details",
|
||||
"description": "Filter teams by various properties, like full auto or button presses"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"charge_attack": "Charge Attack",
|
||||
"full_auto": "Full Auto",
|
||||
|
|
@ -263,7 +273,9 @@
|
|||
"min_weapons": "Minimum number of weapons",
|
||||
"name_quality": "Hide untitled teams",
|
||||
"user_quality": "Hide anonymous users",
|
||||
"original_only": "Hide remixed teams"
|
||||
"original_only": "Hide remixed teams",
|
||||
"inclusion": "Show teams with these items",
|
||||
"exclusion": "Hide teams with these items"
|
||||
},
|
||||
"notice": "Filters set on <strong>user profiles</strong> and in <strong>your saved teams</strong> will not be saved",
|
||||
"options": {
|
||||
|
|
@ -271,6 +283,15 @@
|
|||
"off": "Off",
|
||||
"on_and_off": "On and Off"
|
||||
},
|
||||
"placeholders": {
|
||||
"inclusion": "Teams that have...",
|
||||
"exclusion": "Teams that don't have..."
|
||||
},
|
||||
"prompts": {
|
||||
"type": "Start typing...",
|
||||
"searching": "Searching...",
|
||||
"not_found": "No results found"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "Save filters",
|
||||
"clear": "Reset to default"
|
||||
|
|
@ -488,7 +509,8 @@
|
|||
"min_length": "Type at least 3 characters",
|
||||
"no_results": "No results found for '{{query}}'",
|
||||
"no_results_generic": "No results found",
|
||||
"end_results": "No more results"
|
||||
"end_results": "No more results",
|
||||
"type": "Keep typing..."
|
||||
},
|
||||
"placeholders": {
|
||||
"weapon": "Search for a weapon...",
|
||||
|
|
|
|||
|
|
@ -249,6 +249,16 @@
|
|||
},
|
||||
"filters": {
|
||||
"title": "フィルター設定",
|
||||
"headers": {
|
||||
"items": {
|
||||
"name": "アイテムをフィルター",
|
||||
"description": "キャラクター・武器・召喚石が編成に包含・除外されているかに基づいて表示する"
|
||||
},
|
||||
"details": {
|
||||
"name": "詳細をフィルター",
|
||||
"description": "フルオートやポチなどの編成詳細に基づいて表示する"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"charge_attack": "奥義",
|
||||
"full_auto": "フルオート",
|
||||
|
|
@ -260,7 +270,9 @@
|
|||
"min_weapons": "最小武器数",
|
||||
"name_quality": "無題の編成なし",
|
||||
"user_quality": "無名のユーザーなし",
|
||||
"original_only": "リミックスなし"
|
||||
"original_only": "リミックスなし",
|
||||
"inclusion": "包含されているアイテム",
|
||||
"exclusion": "除外されているアイテムめn"
|
||||
},
|
||||
"notice": "フィルターは<strong>保存した編成</strong>と<strong>ユーザープロフィール</strong>には保存されません",
|
||||
"options": {
|
||||
|
|
@ -268,6 +280,15 @@
|
|||
"off": "OFF",
|
||||
"on_and_off": "両方"
|
||||
},
|
||||
"placeholders": {
|
||||
"inclusion": "〇〇が入っている",
|
||||
"exclusion": "〇〇が入っていない"
|
||||
},
|
||||
"prompts": {
|
||||
"type": "入力してください",
|
||||
"searching": "検索中...",
|
||||
"not_found": "見つかりませんでした"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "フィルターを保存する",
|
||||
"clear": "保存したフィルターをリセット"
|
||||
|
|
@ -486,7 +507,8 @@
|
|||
"min_length": "3文字以上を入力してください",
|
||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||
"no_results_generic": "検索結果が見つかりませんでした",
|
||||
"end_results": "検索結果これ以上ありません"
|
||||
"end_results": "検索結果これ以上ありません",
|
||||
"type": "もっと入力してください"
|
||||
},
|
||||
"placeholders": {
|
||||
"weapon": "武器を検索...",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@
|
|||
--anonymous-bg: #{$anonymous--bg--light};
|
||||
--placeholder-bg: #{$grey-80};
|
||||
|
||||
--transparent-stroke: #{$transparent--stroke--light};
|
||||
|
||||
// Light - Units
|
||||
--unit-bg: #{$unit--bg--light};
|
||||
--unit-bg-hover: #{$unit--bg--light--hover};
|
||||
|
|
@ -252,6 +254,8 @@
|
|||
--anonymous-bg: #{$anonymous--bg--dark};
|
||||
--placeholder-bg: #{$grey-40};
|
||||
|
||||
--transparent-stroke: #{$transparent--stroke--dark};
|
||||
|
||||
// Dark - Units
|
||||
--unit-bg: #{$unit--bg--dark};
|
||||
--unit-bg-hover: #{$unit--bg--dark--hover};
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ $dark-bg-00: #ba63d8;
|
|||
$dark-bg-10: #de7bff;
|
||||
$dark-bg-20: #f2cdff;
|
||||
|
||||
$transparent--stroke--light: rgba(0, 0, 0, 0.9);
|
||||
$transparent--stroke--dark: rgba(255, 255, 255, 0.35);
|
||||
|
||||
$page--bg--light: $grey-90;
|
||||
$page--bg--dark: $grey-15;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
|
|||
2
types/FilterSet.d.ts
vendored
2
types/FilterSet.d.ts
vendored
|
|
@ -13,4 +13,6 @@ interface FilterSet {
|
|||
name_quality?: boolean
|
||||
user_quality?: boolean
|
||||
original?: boolean
|
||||
includes?: MentionItem[]
|
||||
excludes?: MentionItem[]
|
||||
}
|
||||
|
|
|
|||
10
types/MentionItem.d.ts
vendored
Normal file
10
types/MentionItem.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface MentionItem {
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
type: string
|
||||
granblue_id: string
|
||||
element: GranblueElement
|
||||
}
|
||||
|
|
@ -70,11 +70,15 @@ class Api {
|
|||
})
|
||||
}
|
||||
|
||||
searchAll(query: string, locale: string) {
|
||||
searchAll(query: string, exclude: string[], locale: string) {
|
||||
const resourceUrl = `${this.url}/search`
|
||||
// Also send list of Granblue IDs
|
||||
// so the backend can exclude opposites and duplicates
|
||||
// Maybe store them in state???
|
||||
return axios.post(`${resourceUrl}`, {
|
||||
search: {
|
||||
query: query,
|
||||
exclude: exclude,
|
||||
locale: locale
|
||||
}
|
||||
})
|
||||
|
|
|
|||
21
utils/convertAdvancedFilters.tsx
Normal file
21
utils/convertAdvancedFilters.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
export function convertAdvancedFilters(filters: FilterSet) {
|
||||
let copy = cloneDeep(filters)
|
||||
|
||||
const includes = filterString(filters.includes || [])
|
||||
const excludes = filterString(filters.excludes || [])
|
||||
|
||||
delete copy.includes
|
||||
delete copy.excludes
|
||||
|
||||
return {
|
||||
...copy,
|
||||
includes,
|
||||
excludes,
|
||||
}
|
||||
}
|
||||
|
||||
export function filterString(list: MentionItem[]) {
|
||||
return list.map((item) => item.granblue_id).join(',')
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import type { JSONContent } from '@tiptap/core'
|
||||
import { ReactRenderer } from '@tiptap/react'
|
||||
import { MentionOptions } from '@tiptap/extension-mention'
|
||||
import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion'
|
||||
import tippy, { Instance as TippyInstance } from 'tippy.js'
|
||||
import { getCookie } from 'cookies-next'
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ import {
|
|||
} from '~components/MentionList'
|
||||
import api from '~utils/api'
|
||||
import { numberToElement } from '~utils/elements'
|
||||
import { get } from 'http'
|
||||
import { SuggestionOptions } from '~extensions/CustomSuggestion'
|
||||
|
||||
interface RawSearchResponse {
|
||||
searchable_type: string
|
||||
|
|
@ -45,12 +44,22 @@ function transform(object: RawSearchResponse) {
|
|||
return result
|
||||
}
|
||||
|
||||
export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
||||
items: async ({ query }): Promise<MentionSuggestion[]> => {
|
||||
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)]
|
||||
}
|
||||
|
||||
export const mentionSuggestionOptions: Omit<SuggestionOptions, 'editor'> = {
|
||||
items: async ({ query, editor }): Promise<MentionSuggestion[]> => {
|
||||
const locale = getCookie('NEXT_LOCALE')
|
||||
? (getCookie('NEXT_LOCALE') as string)
|
||||
: 'en'
|
||||
const response = await api.searchAll(query, locale)
|
||||
const response = await api.searchAll(query, [], locale)
|
||||
const results = response.data.results
|
||||
|
||||
return results
|
||||
|
|
@ -72,7 +81,6 @@ export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
|||
render: () => {
|
||||
let component: ReactRenderer<MentionRef> | undefined
|
||||
let popup: TippyInstance | undefined
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
|
|
@ -80,6 +88,10 @@ export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
|||
editor: props.editor,
|
||||
})
|
||||
|
||||
console.log('in onStart')
|
||||
const rect = props.clientRect?.()
|
||||
console.log(rect)
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
|
|
@ -105,6 +117,10 @@ export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
|||
return true
|
||||
}
|
||||
|
||||
if (props.event.key === 'Tab') {
|
||||
popup?.hide()
|
||||
}
|
||||
|
||||
if (!component?.ref) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue