switch to nx monorepo

This commit is contained in:
Philipinho
2024-01-09 18:58:26 +01:00
parent e1bb2632b8
commit 093e634c0b
273 changed files with 11419 additions and 31 deletions

View File

@ -0,0 +1,132 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
SlashMenuGroupedItemsType,
SlashMenuItemType,
} from '@/features/editor/components/slash-menu/types';
import {
Group,
Paper,
ScrollArea,
Text,
UnstyledButton,
} from '@mantine/core';
import classes from './slash-menu.module.css';
import clsx from 'clsx';
const CommandList = ({
items,
command,
editor,
range,
}: {
items: SlashMenuGroupedItemsType;
command: any;
editor: any;
range: any;
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const viewportRef = useRef<HTMLDivElement>(null);
const flatItems = useMemo(() => {
return Object.values(items).flat();
}, [items]);
const selectItem = useCallback(
(index: number) => {
const item = flatItems[index];
if (item) {
command(item);
}
},
[command, flatItems],
);
useEffect(() => {
const navigationKeys = ['ArrowUp', 'ArrowDown', 'Enter'];
const onKeyDown = (e: KeyboardEvent) => {
if (navigationKeys.includes(e.key)) {
e.preventDefault();
if (e.key === 'ArrowUp') {
setSelectedIndex((selectedIndex + flatItems.length - 1) % flatItems.length);
return true;
}
if (e.key === 'ArrowDown') {
setSelectedIndex((selectedIndex + 1) % flatItems.length);
return true;
}
if (e.key === 'Enter') {
selectItem(selectedIndex);
return true;
}
return false;
}
};
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [flatItems, selectedIndex, setSelectedIndex, selectItem]);
useEffect(() => {
setSelectedIndex(0);
}, [flatItems]);
useEffect(() => {
viewportRef.current
?.querySelector(`[data-item-index="${selectedIndex}"]`)
?.scrollIntoView({ block: 'nearest' });
}, [selectedIndex]);
return flatItems.length > 0 ? (
<Paper id="slash-command" shadow="xl" p="sm" withBorder>
<ScrollArea viewportRef={viewportRef} h={350} w={250} scrollbarSize={5}>
{Object.entries(items).map(([category, categoryItems]) => (
<div key={category}>
<Text c="dimmed" mb={4} fw={500} tt="capitalize">
{category}
</Text>
{categoryItems.map((item: SlashMenuItemType, index: number) => (
<UnstyledButton
data-item-index={index}
key={index}
onClick={() => selectItem(index)}
className={clsx(classes.menuBtn, { [classes.selectedItem]: index === selectedIndex })}
style={{
width: '100%',
padding: 'var(--mantine-spacing-xs)',
color: 'var(--mantine-color-text)',
borderRadius: 'var(--mantine-radius-sm)',
}}
>
<Group>
<item.icon size={18} />
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{item.title}
</Text>
<Text c="dimmed" size="xs">
{item.description}
</Text>
</div>
</Group>
</UnstyledButton>
))}
</div>
))}
</ScrollArea>
</Paper>
) : null;
};
export default CommandList;

View File

@ -0,0 +1,163 @@
import {
IconBlockquote,
IconCheckbox, IconCode,
IconH1,
IconH2,
IconH3,
IconList,
IconListNumbers, IconPhoto,
IconTypography,
} from '@tabler/icons-react';
import { CommandProps, SlashMenuGroupedItemsType } from '@/features/editor/components/slash-menu/types';
const CommandGroups: SlashMenuGroupedItemsType = {
basic: [
{
title: 'Text',
description: 'Just start typing with plain text.',
searchTerms: ['p', 'paragraph'],
icon: IconTypography,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode('paragraph', 'paragraph')
.run();
},
},
{
title: 'To-do List',
description: 'Track tasks with a to-do list.',
searchTerms: ['todo', 'task', 'list', 'check', 'checkbox'],
icon: IconCheckbox,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
{
title: 'Heading 1',
description: 'Big section heading.',
searchTerms: ['title', 'big', 'large'],
icon: IconH1,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 1 })
.run();
},
},
{
title: 'Heading 2',
description: 'Medium section heading.',
searchTerms: ['subtitle', 'medium'],
icon: IconH2,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 2 })
.run();
},
},
{
title: 'Heading 3',
description: 'Small section heading.',
searchTerms: ['subtitle', 'small'],
icon: IconH3,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 3 })
.run();
},
},
{
title: 'Bullet List',
description: 'Create a simple bullet list.',
searchTerms: ['unordered', 'point'],
icon: IconList,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
title: 'Numbered List',
description: 'Create a list with numbering.',
searchTerms: ['ordered'],
icon: IconListNumbers,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
},
},
{
title: 'Quote',
description: 'Capture a quote.',
searchTerms: ['blockquote', 'quotes'],
icon: IconBlockquote,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode('paragraph', 'paragraph')
.toggleBlockquote()
.run(),
},
{
title: 'Code',
description: 'Capture a code snippet.',
searchTerms: ['codeblock'],
icon: IconCode,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: 'Image',
description: 'Upload an image from your computer.',
searchTerms: ['photo', 'picture', 'media'],
icon: IconPhoto,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).run();
// upload image
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
//startImageUpload(file, editor.view, pos);
}
};
input.click();
},
},
],
};
export const getSuggestionItems = ({ query }: { query: string }): SlashMenuGroupedItemsType => {
const search = query.toLowerCase();
const filteredGroups: SlashMenuGroupedItemsType = {};
for (const [group, items] of Object.entries(CommandGroups)) {
const filteredItems = items.filter((item) => {
return item.title.toLowerCase().includes(search)
|| item.description.toLowerCase().includes(search)
|| (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)));
});
if (filteredItems.length) {
filteredGroups[group] = filteredItems;
}
}
return filteredGroups;
};
export default getSuggestionItems;

View File

@ -0,0 +1,66 @@
import { Editor } from '@tiptap/core';
import { ReactRenderer } from '@tiptap/react';
import CommandList from '@/features/editor/components/slash-menu/command-list';
import tippy from 'tippy.js';
const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(CommandList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
// @ts-ignore
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
component?.updateProps(props);
if (!props.clientRect) {
return;
}
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === 'Escape') {
popup?.[0].hide();
return true;
}
// @ts-ignore
return component?.ref?.onKeyDown(props);
},
onExit: () => {
if (popup && !popup[0].state.isDestroyed) {
popup[0].destroy();
}
if (component) {
component.destroy();
}
},
};
};
export default renderItems;

View File

@ -0,0 +1,21 @@
.menuBtn {
&:hover {
@mixin light {
background: var(--mantine-color-gray-2);
}
@mixin dark {
background: var(--mantine-color-gray-light);
}
}
}
.selectedItem {
@mixin light {
background: var(--mantine-color-gray-2);
}
@mixin dark {
background: var(--mantine-color-gray-light);
}
}

View File

@ -0,0 +1,27 @@
import { Editor, Range } from '@tiptap/core';
export type CommandProps = {
editor: Editor;
range: Range;
}
export type CommandListProps = {
items: SlashMenuGroupedItemsType;
command: (item: SlashMenuItemType) => void;
editor: Editor;
range: Range;
}
export type SlashMenuItemType = {
title: string;
description: string;
icon: any;
separator?: true;
searchTerms: string[];
command: (props: CommandProps) => void;
disable?: (editor: Editor) => boolean;
}
export type SlashMenuGroupedItemsType = {
[category: string]: SlashMenuItemType[];
};