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'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
type Props = Pick<SuggestionProps, 'items' | 'command'>
|
type Props = Pick<SuggestionProps, 'items' | 'command' | 'query'>
|
||||||
|
|
||||||
export type MentionRef = {
|
export type MentionRef = {
|
||||||
onKeyDown: (props: { event: KeyboardEvent }) => boolean
|
onKeyDown: (props: { event: KeyboardEvent }) => boolean
|
||||||
|
|
@ -113,7 +113,9 @@ export const MentionList = forwardRef<MentionRef, Props>(
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.noResult}>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -277,8 +277,8 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="Account"
|
className="Account"
|
||||||
headerref={headerRef}
|
headerRef={headerRef}
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onOpenAutoFocus={(event: Event) => {}}
|
onOpenAutoFocus={(event: Event) => {}}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ const LoginModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="login"
|
className="login"
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ const SignupModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="signup"
|
className="signup"
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const CharacterConflictModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="conflict"
|
className="conflict"
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={close}
|
onEscapeKeyDown={close}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -354,8 +354,8 @@ const CharacterModal = ({
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="character"
|
className="character"
|
||||||
headerref={headerRef}
|
headerRef={headerRef}
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={() => {}}
|
onEscapeKeyDown={() => {}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,15 @@ interface Props
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
> {
|
> {
|
||||||
wrapperClassName?: string
|
wrapperClassName?: string
|
||||||
headerref?: React.RefObject<HTMLDivElement>
|
headerRef?: React.RefObject<HTMLDivElement>
|
||||||
footerref?: React.RefObject<HTMLDivElement>
|
footerRef?: React.RefObject<HTMLDivElement>
|
||||||
scrollable?: boolean
|
scrollable?: boolean
|
||||||
onEscapeKeyDown: (event: KeyboardEvent) => void
|
onEscapeKeyDown: (event: KeyboardEvent) => void
|
||||||
onOpenAutoFocus: (event: Event) => void
|
onOpenAutoFocus: (event: Event) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
||||||
{ scrollable, children, ...props },
|
{ scrollable, wrapperClassName, headerRef, footerRef, children, ...props },
|
||||||
forwardedRef
|
forwardedRef
|
||||||
) {
|
) {
|
||||||
// Classes
|
// Classes
|
||||||
|
|
@ -37,12 +37,12 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
||||||
const scrollHeight = event.currentTarget.scrollHeight
|
const scrollHeight = event.currentTarget.scrollHeight
|
||||||
const clientHeight = event.currentTarget.clientHeight
|
const clientHeight = event.currentTarget.clientHeight
|
||||||
|
|
||||||
if (props.headerref && props.headerref.current)
|
if (headerRef && headerRef.current)
|
||||||
manipulateHeaderShadow(props.headerref.current, scrollTop)
|
manipulateHeaderShadow(headerRef.current, scrollTop)
|
||||||
|
|
||||||
if (props.footerref && props.footerref.current)
|
if (footerRef && footerRef.current)
|
||||||
manipulateFooterShadow(
|
manipulateFooterShadow(
|
||||||
props.footerref.current,
|
footerRef.current,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
scrollHeight,
|
scrollHeight,
|
||||||
clientHeight
|
clientHeight
|
||||||
|
|
@ -94,7 +94,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
||||||
const calculateFooterShadow = debounce(() => {
|
const calculateFooterShadow = debounce(() => {
|
||||||
const boxShadowBase = '0 -2px 8px'
|
const boxShadowBase = '0 -2px 8px'
|
||||||
const scrollable = document.querySelector(`.${styles.scrollable}`)
|
const scrollable = document.querySelector(`.${styles.scrollable}`)
|
||||||
const footer = props.footerref
|
const footer = footerRef
|
||||||
|
|
||||||
if (footer && footer.current) {
|
if (footer && footer.current) {
|
||||||
if (scrollable && scrollable.clientHeight >= scrollable.scrollHeight) {
|
if (scrollable && scrollable.clientHeight >= scrollable.scrollHeight) {
|
||||||
|
|
@ -133,9 +133,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
||||||
{
|
{
|
||||||
[styles.dialog]: true,
|
[styles.dialog]: true,
|
||||||
},
|
},
|
||||||
props.wrapperClassName
|
wrapperClassName?.split(' ').map((className) => styles[className])
|
||||||
?.split(' ')
|
|
||||||
.map((className) => styles[className])
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
|
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
transition: box-shadow 0.1s ease-out, border-top 0.1s ease-out;
|
transition: box-shadow 0.1s ease-out, border-top 0.1s ease-out;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
.left,
|
.left,
|
||||||
.right {
|
.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 {
|
.left {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
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 {
|
.notice {
|
||||||
background-color: var(--notice-bg);
|
background-color: var(--notice-bg);
|
||||||
border-radius: $input-corner;
|
border-radius: $input-corner;
|
||||||
|
|
@ -34,11 +50,16 @@
|
||||||
.fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit-2x;
|
gap: $unit-4x;
|
||||||
padding: 0 $unit-4x;
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-2x;
|
||||||
|
padding: 0 $unit-4x;
|
||||||
|
|
||||||
@include breakpoint(phone) {
|
@include breakpoint(phone) {
|
||||||
gap: $unit-4x;
|
gap: $unit-4x;
|
||||||
margin-bottom: $unit * 24;
|
margin-bottom: $unit * 24;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import SelectItem from '~components/common/SelectItem'
|
||||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
import MentionTableField from '~components/common/MentionTableField'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
defaultFilterSet: FilterSet
|
defaultFilterSet: FilterSet
|
||||||
|
|
@ -47,6 +49,8 @@ const FilterModal = (props: Props) => {
|
||||||
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
|
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
|
||||||
const [fullAutoOpen, setFullAutoOpen] = useState(false)
|
const [fullAutoOpen, setFullAutoOpen] = useState(false)
|
||||||
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
|
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
|
||||||
|
const [inclusions, setInclusions] = useState<MentionItem[]>([])
|
||||||
|
const [exclusions, setExclusions] = useState<MentionItem[]>([])
|
||||||
const [filterSet, setFilterSet] = useState<FilterSet>({})
|
const [filterSet, setFilterSet] = useState<FilterSet>({})
|
||||||
|
|
||||||
// Filter states
|
// Filter states
|
||||||
|
|
@ -82,11 +86,17 @@ const FilterModal = (props: Props) => {
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.open !== undefined) setOpen(props.open)
|
if (props.open !== undefined) {
|
||||||
|
setOpen(props.open)
|
||||||
|
|
||||||
|
// When should we reset the filter state?
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilterSet(props.filterSet)
|
setFilterSet(props.filterSet)
|
||||||
|
setInclusions(props.filterSet.includes || [])
|
||||||
|
setExclusions(props.filterSet.excludes || [])
|
||||||
}, [props.filterSet])
|
}, [props.filterSet])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -127,6 +137,9 @@ const FilterModal = (props: Props) => {
|
||||||
if (maxButtonsCount) filters.button_count = maxButtonsCount
|
if (maxButtonsCount) filters.button_count = maxButtonsCount
|
||||||
if (maxTurnsCount) filters.turn_count = maxTurnsCount
|
if (maxTurnsCount) filters.turn_count = maxTurnsCount
|
||||||
|
|
||||||
|
if (inclusions.length > 0) filters.includes = inclusions
|
||||||
|
if (exclusions.length > 0) filters.excludes = exclusions
|
||||||
|
|
||||||
if (props.persistFilters) {
|
if (props.persistFilters) {
|
||||||
setCookie('filters', filters, { path: '/' })
|
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 = () => {
|
const filterNotice = () => {
|
||||||
if (props.persistFilters) return null
|
if (props.persistFilters) return null
|
||||||
return (
|
return (
|
||||||
|
|
@ -404,25 +448,39 @@ const FilterModal = (props: Props) => {
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="filter"
|
className="filter"
|
||||||
wrapperClassName="filter"
|
wrapperClassName="filter"
|
||||||
headerref={headerRef}
|
headerRef={headerRef}
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
>
|
>
|
||||||
<DialogHeader title={t('modals.filters.title')} />
|
<DialogHeader title={t('modals.filters.title')} />
|
||||||
<div className={styles.fields}>
|
<div className={styles.fields}>
|
||||||
{filterNotice()}
|
{filterNotice()}
|
||||||
{chargeAttackField()}
|
<section>
|
||||||
{fullAutoField()}
|
<div className={styles.header}>
|
||||||
{autoGuardField()}
|
<h3>{t('modals.filters.headers.items.name')}</h3>
|
||||||
{/* {maxButtonsField()} */}
|
<p>{t('modals.filters.headers.items.description')}</p>
|
||||||
{/* {maxTurnsField()} */}
|
</div>
|
||||||
{minCharactersField()}
|
{inclusionField}
|
||||||
{minSummonsField()}
|
{exclusionField}
|
||||||
{minWeaponsField()}
|
</section>
|
||||||
{nameQualityField()}
|
<section>
|
||||||
{userQualityField()}
|
<div className={styles.header}>
|
||||||
{originalOnlyField()}
|
<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>
|
</div>
|
||||||
<DialogFooter
|
<DialogFooter
|
||||||
ref={footerRef}
|
ref={footerRef}
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ const SearchModal = (props: Props) => {
|
||||||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="search"
|
className="search"
|
||||||
headerref={headerRef}
|
headerRef={headerRef}
|
||||||
scrollable={false}
|
scrollable={false}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ const WeaponConflictModal = (props: Props) => {
|
||||||
<Dialog open={open} onOpenChange={openChange}>
|
<Dialog open={open} onOpenChange={openChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="conflict"
|
className="conflict"
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={close}
|
onEscapeKeyDown={close}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -469,8 +469,8 @@ const WeaponModal = ({
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="Weapon"
|
className="Weapon"
|
||||||
headerref={headerRef}
|
headerRef={headerRef}
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
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/react": "^2.0.3",
|
||||||
"@tiptap/starter-kit": "^2.0.3",
|
"@tiptap/starter-kit": "^2.0.3",
|
||||||
"@tiptap/suggestion": "2.0.0-beta.91",
|
"@tiptap/suggestion": "2.0.0-beta.91",
|
||||||
|
"@types/react-bootstrap-typeahead": "^5.1.9",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
|
|
@ -53,6 +54,7 @@
|
||||||
"next-usequerystate": "^1.7.0",
|
"next-usequerystate": "^1.7.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap-typeahead": "^6.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^11.15.5",
|
"react-i18next": "^11.15.5",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
|
@ -4486,6 +4488,17 @@
|
||||||
"type-fest": "^2.19.0"
|
"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": {
|
"node_modules/@rushstack/eslint-patch": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz",
|
||||||
|
|
@ -7890,6 +7903,14 @@
|
||||||
"csstype": "^3.0.2"
|
"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": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "17.0.20",
|
"version": "17.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz",
|
"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==",
|
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.24",
|
"version": "17.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
||||||
|
|
@ -10368,6 +10394,11 @@
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -11076,7 +11107,6 @@
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
|
@ -11206,6 +11236,15 @@
|
||||||
"utila": "~0.4"
|
"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": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
|
@ -18645,6 +18684,29 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-colorful": {
|
||||||
"version": "5.6.1",
|
"version": "5.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||||
|
|
@ -18727,6 +18789,11 @@
|
||||||
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
|
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/react-i18next": {
|
||||||
"version": "11.18.6",
|
"version": "11.18.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
|
||||||
|
|
@ -18774,6 +18841,11 @@
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/react-linkify": {
|
||||||
"version": "1.0.0-alpha",
|
"version": "1.0.0-alpha",
|
||||||
"resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz",
|
"resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz",
|
||||||
|
|
@ -18792,6 +18864,39 @@
|
||||||
"react-dom": ">=16.0.8"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
|
|
@ -19746,6 +19851,14 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
|
@ -21243,6 +21356,20 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/unfetch": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
||||||
|
|
@ -21761,6 +21888,14 @@
|
||||||
"makeerror": "1.0.12"
|
"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": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"@tiptap/react": "^2.0.3",
|
"@tiptap/react": "^2.0.3",
|
||||||
"@tiptap/starter-kit": "^2.0.3",
|
"@tiptap/starter-kit": "^2.0.3",
|
||||||
"@tiptap/suggestion": "2.0.0-beta.91",
|
"@tiptap/suggestion": "2.0.0-beta.91",
|
||||||
|
"@types/react-bootstrap-typeahead": "^5.1.9",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
|
|
@ -60,6 +61,7 @@
|
||||||
"next-usequerystate": "^1.7.0",
|
"next-usequerystate": "^1.7.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap-typeahead": "^6.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^11.15.5",
|
"react-i18next": "^11.15.5",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
|
|
||||||
|
|
@ -158,9 +158,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
{!appState.version ? (
|
{!appState.version ? (
|
||||||
serverUnavailable()
|
serverUnavailable()
|
||||||
) : (
|
) : (
|
||||||
<main>
|
<Component {...pageProps} />
|
||||||
<Component {...pageProps} />
|
|
||||||
</main>
|
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
<Viewport className="ToastViewport" />
|
<Viewport className="ToastViewport" />
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { appState } from '~utils/appState'
|
||||||
import { defaultFilterset } from '~utils/defaultFilters'
|
import { defaultFilterset } from '~utils/defaultFilters'
|
||||||
import { elements, allElement } from '~data/elements'
|
import { elements, allElement } from '~data/elements'
|
||||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||||
|
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
|
||||||
|
|
||||||
import ErrorSection from '~components/ErrorSection'
|
import ErrorSection from '~components/ErrorSection'
|
||||||
import GridRep from '~components/GridRep'
|
import GridRep from '~components/GridRep'
|
||||||
|
|
@ -157,7 +158,7 @@ const TeamsRoute: React.FC<Props> = ({
|
||||||
raid: raid === 'all' ? undefined : raid,
|
raid: raid === 'all' ? undefined : raid,
|
||||||
recency: recency !== -1 ? recency : undefined,
|
recency: recency !== -1 ? recency : undefined,
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
...advancedFilters,
|
...convertAdvancedFilters(advancedFilters),
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(filters).forEach(
|
Object.keys(filters).forEach(
|
||||||
|
|
@ -393,7 +394,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
// Create filter object
|
// Create filter object
|
||||||
const filters: FilterObject = extractFilters(query, raidGroups)
|
const filters: FilterObject = extractFilters(query, raidGroups)
|
||||||
const params = {
|
const params = {
|
||||||
params: { ...filters, ...advancedFilters },
|
params: { ...filters, ...convertAdvancedFilters(advancedFilters) },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up empty variables
|
// Set up empty variables
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,16 @@
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "Advanced 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": {
|
"labels": {
|
||||||
"charge_attack": "Charge Attack",
|
"charge_attack": "Charge Attack",
|
||||||
"full_auto": "Full Auto",
|
"full_auto": "Full Auto",
|
||||||
|
|
@ -263,7 +273,9 @@
|
||||||
"min_weapons": "Minimum number of weapons",
|
"min_weapons": "Minimum number of weapons",
|
||||||
"name_quality": "Hide untitled teams",
|
"name_quality": "Hide untitled teams",
|
||||||
"user_quality": "Hide anonymous users",
|
"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",
|
"notice": "Filters set on <strong>user profiles</strong> and in <strong>your saved teams</strong> will not be saved",
|
||||||
"options": {
|
"options": {
|
||||||
|
|
@ -271,6 +283,15 @@
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"on_and_off": "On and 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": {
|
"buttons": {
|
||||||
"confirm": "Save filters",
|
"confirm": "Save filters",
|
||||||
"clear": "Reset to default"
|
"clear": "Reset to default"
|
||||||
|
|
@ -488,7 +509,8 @@
|
||||||
"min_length": "Type at least 3 characters",
|
"min_length": "Type at least 3 characters",
|
||||||
"no_results": "No results found for '{{query}}'",
|
"no_results": "No results found for '{{query}}'",
|
||||||
"no_results_generic": "No results found",
|
"no_results_generic": "No results found",
|
||||||
"end_results": "No more results"
|
"end_results": "No more results",
|
||||||
|
"type": "Keep typing..."
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"weapon": "Search for a weapon...",
|
"weapon": "Search for a weapon...",
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,16 @@
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "フィルター設定",
|
"title": "フィルター設定",
|
||||||
|
"headers": {
|
||||||
|
"items": {
|
||||||
|
"name": "アイテムをフィルター",
|
||||||
|
"description": "キャラクター・武器・召喚石が編成に包含・除外されているかに基づいて表示する"
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"name": "詳細をフィルター",
|
||||||
|
"description": "フルオートやポチなどの編成詳細に基づいて表示する"
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"charge_attack": "奥義",
|
"charge_attack": "奥義",
|
||||||
"full_auto": "フルオート",
|
"full_auto": "フルオート",
|
||||||
|
|
@ -260,7 +270,9 @@
|
||||||
"min_weapons": "最小武器数",
|
"min_weapons": "最小武器数",
|
||||||
"name_quality": "無題の編成なし",
|
"name_quality": "無題の編成なし",
|
||||||
"user_quality": "無名のユーザーなし",
|
"user_quality": "無名のユーザーなし",
|
||||||
"original_only": "リミックスなし"
|
"original_only": "リミックスなし",
|
||||||
|
"inclusion": "包含されているアイテム",
|
||||||
|
"exclusion": "除外されているアイテムめn"
|
||||||
},
|
},
|
||||||
"notice": "フィルターは<strong>保存した編成</strong>と<strong>ユーザープロフィール</strong>には保存されません",
|
"notice": "フィルターは<strong>保存した編成</strong>と<strong>ユーザープロフィール</strong>には保存されません",
|
||||||
"options": {
|
"options": {
|
||||||
|
|
@ -268,6 +280,15 @@
|
||||||
"off": "OFF",
|
"off": "OFF",
|
||||||
"on_and_off": "両方"
|
"on_and_off": "両方"
|
||||||
},
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"inclusion": "〇〇が入っている",
|
||||||
|
"exclusion": "〇〇が入っていない"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"type": "入力してください",
|
||||||
|
"searching": "検索中...",
|
||||||
|
"not_found": "見つかりませんでした"
|
||||||
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"confirm": "フィルターを保存する",
|
"confirm": "フィルターを保存する",
|
||||||
"clear": "保存したフィルターをリセット"
|
"clear": "保存したフィルターをリセット"
|
||||||
|
|
@ -486,7 +507,8 @@
|
||||||
"min_length": "3文字以上を入力してください",
|
"min_length": "3文字以上を入力してください",
|
||||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||||
"no_results_generic": "検索結果が見つかりませんでした",
|
"no_results_generic": "検索結果が見つかりませんでした",
|
||||||
"end_results": "検索結果これ以上ありません"
|
"end_results": "検索結果これ以上ありません",
|
||||||
|
"type": "もっと入力してください"
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"weapon": "武器を検索...",
|
"weapon": "武器を検索...",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@
|
||||||
--anonymous-bg: #{$anonymous--bg--light};
|
--anonymous-bg: #{$anonymous--bg--light};
|
||||||
--placeholder-bg: #{$grey-80};
|
--placeholder-bg: #{$grey-80};
|
||||||
|
|
||||||
|
--transparent-stroke: #{$transparent--stroke--light};
|
||||||
|
|
||||||
// Light - Units
|
// Light - Units
|
||||||
--unit-bg: #{$unit--bg--light};
|
--unit-bg: #{$unit--bg--light};
|
||||||
--unit-bg-hover: #{$unit--bg--light--hover};
|
--unit-bg-hover: #{$unit--bg--light--hover};
|
||||||
|
|
@ -252,6 +254,8 @@
|
||||||
--anonymous-bg: #{$anonymous--bg--dark};
|
--anonymous-bg: #{$anonymous--bg--dark};
|
||||||
--placeholder-bg: #{$grey-40};
|
--placeholder-bg: #{$grey-40};
|
||||||
|
|
||||||
|
--transparent-stroke: #{$transparent--stroke--dark};
|
||||||
|
|
||||||
// Dark - Units
|
// Dark - Units
|
||||||
--unit-bg: #{$unit--bg--dark};
|
--unit-bg: #{$unit--bg--dark};
|
||||||
--unit-bg-hover: #{$unit--bg--dark--hover};
|
--unit-bg-hover: #{$unit--bg--dark--hover};
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,9 @@ $dark-bg-00: #ba63d8;
|
||||||
$dark-bg-10: #de7bff;
|
$dark-bg-10: #de7bff;
|
||||||
$dark-bg-20: #f2cdff;
|
$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--light: $grey-90;
|
||||||
$page--bg--dark: $grey-15;
|
$page--bg--dark: $grey-15;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"target": "es5",
|
"target": "es2015",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
|
||||||
2
types/FilterSet.d.ts
vendored
2
types/FilterSet.d.ts
vendored
|
|
@ -13,4 +13,6 @@ interface FilterSet {
|
||||||
name_quality?: boolean
|
name_quality?: boolean
|
||||||
user_quality?: boolean
|
user_quality?: boolean
|
||||||
original?: 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`
|
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}`, {
|
return axios.post(`${resourceUrl}`, {
|
||||||
search: {
|
search: {
|
||||||
query: query,
|
query: query,
|
||||||
|
exclude: exclude,
|
||||||
locale: locale
|
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 { 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 tippy, { Instance as TippyInstance } from 'tippy.js'
|
||||||
import { getCookie } from 'cookies-next'
|
import { getCookie } from 'cookies-next'
|
||||||
|
|
||||||
|
|
@ -11,7 +10,7 @@ import {
|
||||||
} from '~components/MentionList'
|
} from '~components/MentionList'
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import { numberToElement } from '~utils/elements'
|
import { numberToElement } from '~utils/elements'
|
||||||
import { get } from 'http'
|
import { SuggestionOptions } from '~extensions/CustomSuggestion'
|
||||||
|
|
||||||
interface RawSearchResponse {
|
interface RawSearchResponse {
|
||||||
searchable_type: string
|
searchable_type: string
|
||||||
|
|
@ -45,12 +44,22 @@ function transform(object: RawSearchResponse) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
function parseMentions(data: JSONContent) {
|
||||||
items: async ({ query }): Promise<MentionSuggestion[]> => {
|
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')
|
const locale = getCookie('NEXT_LOCALE')
|
||||||
? (getCookie('NEXT_LOCALE') as string)
|
? (getCookie('NEXT_LOCALE') as string)
|
||||||
: 'en'
|
: 'en'
|
||||||
const response = await api.searchAll(query, locale)
|
const response = await api.searchAll(query, [], locale)
|
||||||
const results = response.data.results
|
const results = response.data.results
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
@ -72,7 +81,6 @@ export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
||||||
render: () => {
|
render: () => {
|
||||||
let component: ReactRenderer<MentionRef> | undefined
|
let component: ReactRenderer<MentionRef> | undefined
|
||||||
let popup: TippyInstance | undefined
|
let popup: TippyInstance | undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onStart: (props) => {
|
onStart: (props) => {
|
||||||
component = new ReactRenderer(MentionList, {
|
component = new ReactRenderer(MentionList, {
|
||||||
|
|
@ -80,6 +88,10 @@ export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
||||||
editor: props.editor,
|
editor: props.editor,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('in onStart')
|
||||||
|
const rect = props.clientRect?.()
|
||||||
|
console.log(rect)
|
||||||
|
|
||||||
popup = tippy('body', {
|
popup = tippy('body', {
|
||||||
getReferenceClientRect: props.clientRect,
|
getReferenceClientRect: props.clientRect,
|
||||||
appendTo: () => document.body,
|
appendTo: () => document.body,
|
||||||
|
|
@ -105,6 +117,10 @@ export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.event.key === 'Tab') {
|
||||||
|
popup?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
if (!component?.ref) {
|
if (!component?.ref) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue