feat: fix editor

This commit is contained in:
Nevo David 2025-07-17 17:23:30 +07:00
parent 55e8d3c715
commit 1e288ab08a
15 changed files with 249 additions and 416 deletions

View File

@ -552,4 +552,23 @@ html[dir='rtl'] [dir='ltr'] {
.preview ul, .preview li {
white-space: nowrap;
}
.ProseMirror h1, .preview h1 {
font-size: 24px;
font-weight: bold;
}
.ProseMirror h2, .preview h2 {
font-size: 20px;
font-weight: bold;
}
.ProseMirror h3, .preview h3 {
font-size: 18px;
font-weight: bold;
}
.preview p {
min-height: 24px;
}

View File

@ -17,7 +17,7 @@ export const GeneralPreviewComponent: FC<{
const mediaDir = useMediaDirectory();
const renderContent = topValue.map((p) => {
const newContent = stripHtmlValidation('normal', p.content, true)
const newContent = stripHtmlValidation('plain', p.content, true)
.replace(/(@.+?)(\s)/gi, (match, match1, match2) => {
return `<span class="font-bold" style="color: #ae8afc">${match1.trim()}${match2}</span>`;
})

View File

@ -3,13 +3,6 @@
import { EventEmitter } from 'events';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import {
executeCommand,
ExecuteState,
ICommand,
selectWord,
TextAreaTextApi,
} from '@uiw/react-md-editor';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { Input } from '@gitroom/react/form/input';
import { Button } from '@gitroom/react/form/button';
@ -178,52 +171,4 @@ export const LinkedinCompany: FC<{
</div>
);
};
export const linkedinCompany = (identifier: string, id: string): ICommand[] => {
if (identifier !== 'linkedin' && identifier !== 'linkedin-page') {
return [];
}
return [
{
name: 'linkedinCompany',
keyCommand: 'linkedinCompany',
shortcuts: 'ctrlcmd+p',
prefix: '',
suffix: '',
buttonProps: {
'aria-label': 'Add Post Url',
title: 'Add Post Url',
},
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 32 32"
fill="currentColor"
>
<path
d="M27 3H5C4.46957 3 3.96086 3.21071 3.58579 3.58579C3.21071 3.96086 3 4.46957 3 5V27C3 27.5304 3.21071 28.0391 3.58579 28.4142C3.96086 28.7893 4.46957 29 5 29H27C27.5304 29 28.0391 28.7893 28.4142 28.4142C28.7893 28.0391 29 27.5304 29 27V5C29 4.46957 28.7893 3.96086 28.4142 3.58579C28.0391 3.21071 27.5304 3 27 3ZM27 27H5V5H27V27ZM12 14V22C12 22.2652 11.8946 22.5196 11.7071 22.7071C11.5196 22.8946 11.2652 23 11 23C10.7348 23 10.4804 22.8946 10.2929 22.7071C10.1054 22.5196 10 22.2652 10 22V14C10 13.7348 10.1054 13.4804 10.2929 13.2929C10.4804 13.1054 10.7348 13 11 13C11.2652 13 11.5196 13.1054 11.7071 13.2929C11.8946 13.4804 12 13.7348 12 14ZM23 17.5V22C23 22.2652 22.8946 22.5196 22.7071 22.7071C22.5196 22.8946 22.2652 23 22 23C21.7348 23 21.4804 22.8946 21.2929 22.7071C21.1054 22.5196 21 22.2652 21 22V17.5C21 16.837 20.7366 16.2011 20.2678 15.7322C19.7989 15.2634 19.163 15 18.5 15C17.837 15 17.2011 15.2634 16.7322 15.7322C16.2634 16.2011 16 16.837 16 17.5V22C16 22.2652 15.8946 22.5196 15.7071 22.7071C15.5196 22.8946 15.2652 23 15 23C14.7348 23 14.4804 22.8946 14.2929 22.7071C14.1054 22.5196 14 22.2652 14 22V14C14.0012 13.7551 14.0923 13.5191 14.256 13.3369C14.4197 13.1546 14.6446 13.0388 14.888 13.0114C15.1314 12.9839 15.3764 13.0468 15.5765 13.188C15.7767 13.3292 15.918 13.539 15.9738 13.7775C16.6502 13.3186 17.4389 13.0526 18.2552 13.0081C19.0714 12.9637 19.8844 13.1424 20.6067 13.5251C21.329 13.9078 21.9335 14.48 22.3551 15.1803C22.7768 15.8806 22.9997 16.6825 23 17.5ZM12.5 10.5C12.5 10.7967 12.412 11.0867 12.2472 11.3334C12.0824 11.58 11.8481 11.7723 11.574 11.8858C11.2999 11.9994 10.9983 12.0291 10.7074 11.9712C10.4164 11.9133 10.1491 11.7704 9.93934 11.5607C9.72956 11.3509 9.5867 11.0836 9.52882 10.7926C9.47094 10.5017 9.50065 10.2001 9.61418 9.92597C9.72771 9.65189 9.91997 9.41762 10.1666 9.2528C10.4133 9.08797 10.7033 9 11 9C11.3978 9 11.7794 9.15804 12.0607 9.43934C12.342 9.72064 12.5 10.1022 12.5 10.5Z"
fill="currentColor"
/>
</svg>
),
execute: async (state: ExecuteState, api: TextAreaTextApi) => {
const newSelectionRange = selectWord({
text: state.text,
selection: state.selection,
prefix: state.command.prefix!,
suffix: state.command.suffix,
});
const state1 = api.setSelectionRange(newSelectionRange);
const media = await showPostSelector(id);
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: media,
suffix: '',
});
},
},
];
};

View File

@ -1,89 +0,0 @@
import React from 'react';
import {
executeCommand,
ExecuteState,
ICommand,
selectWord,
TextAreaTextApi,
} from '@uiw/react-md-editor';
import { showMediaBox } from '@gitroom/frontend/components/media/media.component';
import { loadVars } from '@gitroom/react/helpers/variable.context';
export const newImage: ICommand = {
name: 'image',
keyCommand: 'image',
shortcuts: 'ctrlcmd+k',
prefix: '![image](',
suffix: ')',
buttonProps: {
'aria-label': 'Add image (ctrl + k)',
title: 'Add image (ctrl + k)',
},
icon: (
<svg width="13" height="13" viewBox="0 0 20 20">
<path
fill="currentColor"
d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"
/>
</svg>
),
execute: (state: ExecuteState, api: TextAreaTextApi) => {
const { uploadDirectory, backendUrl } = loadVars();
let newSelectionRange = selectWord({
text: state.text,
selection: state.selection,
prefix: state.command.prefix!,
suffix: state.command.suffix,
});
let state1 = api.setSelectionRange(newSelectionRange);
if (
state1.selectedText.includes('http') ||
state1.selectedText.includes('www') ||
state1.selectedText.includes('(post:')
) {
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: state.command.prefix!,
suffix: state.command.suffix,
});
return;
}
newSelectionRange = selectWord({
text: state.text,
selection: state.selection,
prefix: '![',
suffix: ']()',
});
state1 = api.setSelectionRange(newSelectionRange);
showMediaBox((media) => {
if (media) {
if (state1.selectedText.length > 0) {
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: '![',
suffix: `](${
media.path.indexOf('http') === -1
? `${backendUrl}/${uploadDirectory}`
: ``
}${media.path})`,
});
return;
}
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: '![image',
suffix: `](${
media.path.indexOf('http') === -1
? `${backendUrl}/${uploadDirectory}`
: ``
}${media.path})`,
});
}
});
},
};

View File

@ -8,11 +8,12 @@ import React, {
useRef,
useState,
ClipboardEvent,
forwardRef,
useImperativeHandle,
} from 'react';
import clsx from 'clsx';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { Transforms } from 'slate';
import EmojiPicker from 'emoji-picker-react';
import { Theme } from 'emoji-picker-react';
import { BoldText } from '@gitroom/frontend/components/new-launch/bold.text';
@ -47,6 +48,8 @@ import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validatio
import { History } from '@tiptap/extension-history';
import { BulletList, ListItem } from '@tiptap/extension-list';
import { Bullets } from '@gitroom/frontend/components/new-launch/bullets.component';
import Heading from '@tiptap/extension-heading';
import { HeadingComponent } from '@gitroom/frontend/components/new-launch/heading.component';
const InterceptBoldShortcut = Extension.create({
name: 'preventBoldWithUnderline',
@ -179,7 +182,7 @@ export const EditorWrapper: FC<{
postComment: state.postComment,
editor: state.editor,
loadedState: state.loaded,
setLoadedState: state.setLoaded
setLoadedState: state.setLoaded,
}))
);
@ -195,6 +198,13 @@ export const EditorWrapper: FC<{
setLoaded(true);
}, [loaded, loadedState]);
useEffect(() => {
if (editor) {
setLoaded(false);
setLoadedState(false);
}
}, [editor]);
const canEdit = useMemo(() => {
return current === 'global' || !!internal;
}, [current, internal]);
@ -493,6 +503,7 @@ export const Editor: FC<{
const newRef = useRef<any>(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const t = useT();
const editorRef = useRef<undefined | { editor: any }>();
const uppy = useUppyUploader({
onUploadSuccess: (result: any) => {
@ -534,94 +545,59 @@ export const Editor: FC<{
const { getRootProps, isDragActive } = useDropzone({ onDrop });
const editorOptions = useMemo(() => {
if (editorType === 'normal') {
return [];
}
const list = [];
if (
editorType === ('markdown' as const) ||
editorType === ('html' as const)
) {
list.push(BulletList, ListItem);
}
return list;
}, [editorType]);
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
Underline,
Bold,
InterceptBoldShortcut,
InterceptUnderlineShortcut,
Span,
History.configure({
depth: 100, // default is 100
newGroupDelay: 100, // default is 500ms
}),
...editorOptions,
],
content: props.value || '',
shouldRerenderOnTransaction: true,
immediatelyRender: false,
// @ts-ignore
onPaste: paste,
onUpdate: (innerProps) => {
props?.onChange?.(innerProps.editor.getHTML());
},
});
const valueWithoutHtml = useMemo(() => {
return stripHtmlValidation('normal', props.value || '');
return stripHtmlValidation('normal', props.value || '', false, true);
}, [props.value]);
const addText = useCallback(
(emoji: string) => {
editor?.commands.insertContent(emoji);
editor?.commands.focus();
editorRef?.current?.editor.commands.insertContent(emoji);
editorRef?.current?.editor.commands.focus();
},
[props.value, id]
);
const addLinkedinTag = useCallback(
(text: string) => {
const id = text.split('(')[1].split(')')[0];
const name = text.split('[')[1].split(']')[0];
const addLinkedinTag = useCallback((text: string) => {
const id = text.split('(')[1].split(')')[0];
const name = text.split('[')[1].split(']')[0];
editor
?.chain()
.focus()
.insertContent({
type: 'mention',
attrs: {
linkedinId: id,
label: name,
},
})
.run();
},
[editor]
);
if (!editor) {
return null;
}
editorRef?.current?.editor
.chain()
.focus()
.insertContent({
type: 'mention',
attrs: {
linkedinId: id,
label: name,
},
})
.run();
}, []);
return (
<div>
<div className="relative bg-customColor2" id={id}>
<div className="flex gap-[5px] bg-customColor55 border-b border-t border-customColor3 justify-center items-center p-[5px]">
<SignatureBox editor={editor} />
<UText editor={editor} currentValue={props.value!} />
<BoldText editor={editor} currentValue={props.value!} />
<SignatureBox editor={editorRef?.current?.editor} />
<UText
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
<BoldText
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
{(editorType === 'markdown' || editorType === 'html') && (
<Bullets editor={editor} currentValue={props.value!} />
<>
<Bullets
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
<HeadingComponent
editor={editorRef?.current?.editor}
currentValue={props.value!}
/>
</>
)}
<div
className="select-none cursor-pointer w-[40px] p-[5px] text-center"
@ -663,16 +639,22 @@ export const Editor: FC<{
Drop your files here to upload
</div>
<div className="px-[10px] pt-[10px]">
<EditorContent editor={editor} />
<OnlyEditor
value={props.value}
editorType={editorType}
onChange={props.onChange}
paste={paste}
ref={editorRef}
/>
</div>
<div
className="w-full h-[46px] overflow-hidden absolute left-0"
onClick={() => {
if (editor?.isFocused) {
if (editorRef?.current?.editor?.isFocused) {
return;
}
editor?.commands?.focus('end');
editorRef?.current?.editor?.commands?.focus('end');
}}
>
<Dashboard
@ -691,10 +673,10 @@ export const Editor: FC<{
<div
className="flex bg-customColor2"
onClick={() => {
if (editor?.isFocused) {
if (editorRef?.current?.editor?.isFocused) {
return;
}
editor?.commands?.focus('end');
editorRef?.current?.editor?.commands?.focus('end');
}}
>
{setImages && (
@ -732,3 +714,68 @@ export const Editor: FC<{
</div>
);
};
export const OnlyEditor = forwardRef<
any,
{
editorType: 'normal' | 'markdown' | 'html';
value: string;
onChange: (value: string) => void;
paste?: (event: ClipboardEvent | File[]) => void;
}
>(({ editorType, value, onChange, paste }, ref) => {
const editorOptions = useMemo(() => {
if (editorType === 'normal') {
return [];
}
const list = [];
if (
editorType === ('markdown' as const) ||
editorType === ('html' as const)
) {
list.push(
BulletList,
ListItem,
Heading.configure({
levels: [1, 2, 3],
})
);
}
return list;
}, [editorType]);
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
Underline,
Bold,
InterceptBoldShortcut,
InterceptUnderlineShortcut,
Span,
History.configure({
depth: 100, // default is 100
newGroupDelay: 100, // default is 500ms
}),
...editorOptions,
],
content: value || '',
shouldRerenderOnTransaction: true,
immediatelyRender: false,
// @ts-ignore
onPaste: paste,
onUpdate: (innerProps) => {
onChange?.(innerProps.editor.getHTML());
},
});
useImperativeHandle(ref, () => ({
editor,
}));
return <EditorContent editor={editor} />;
});

View File

@ -0,0 +1,73 @@
'use client';
import { FC, useCallback } from 'react';
export const HeadingComponent: FC<{
editor: any;
currentValue: string;
}> = ({ editor }) => {
const setHeading = useCallback((level: number) => () => {
editor.commands.toggleHeading({ level })
}, []);
return (
<div className="select-none cursor-pointer w-[40px] p-[5px] text-center relative group">
<svg
width="20"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585786C0.210714 0.960859 0 1.46957 0 2V22C0 22.5304 0.210714 23.0391 0.585786 23.4142C0.960859 23.7893 1.46957 24 2 24H22C22.5304 24 23.0391 23.7893 23.4142 23.4142C23.7893 23.0391 24 22.5304 24 22V2C24 1.46957 23.7893 0.960859 23.4142 0.585786C23.0391 0.210714 22.5304 0 22 0ZM19 18C19 18.2652 18.8946 18.5196 18.7071 18.7071C18.5196 18.8946 18.2652 19 18 19C17.7348 19 17.4804 18.8946 17.2929 18.7071C17.1054 18.5196 17 18.2652 17 18V13H7V18C7 18.2652 6.89464 18.5196 6.70711 18.7071C6.51957 18.8946 6.26522 19 6 19C5.73478 19 5.48043 18.8946 5.29289 18.7071C5.10536 18.5196 5 18.2652 5 18V6C5 5.73478 5.10536 5.48043 5.29289 5.29289C5.48043 5.10536 5.73478 5 6 5C6.26522 5 6.51957 5.10536 6.70711 5.29289C6.89464 5.48043 7 5.73478 7 6V11H17V6C17 5.73478 17.1054 5.48043 17.2929 5.29289C17.4804 5.10536 17.7348 5 18 5C18.2652 5 18.5196 5.10536 18.7071 5.29289C18.8946 5.48043 19 5.73478 19 6V18Z"
fill="currentColor"
/>
</svg>
<div className="flex p-[10px] gap-[5px] -left-[50%] opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 bg-customColor55 border border-customColor3 z-[100] absolute transition-all">
<div onClick={setHeading(1)}>
<svg
width="20"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585786C0.210714 0.960859 0 1.46957 0 2V22C0 22.5304 0.210714 23.0391 0.585786 23.4142C0.960859 23.7893 1.46957 24 2 24H22C22.5304 24 23.0391 23.7893 23.4142 23.4142C23.7893 23.0391 24 22.5304 24 22V2C24 1.46957 23.7893 0.960859 23.4142 0.585786C23.0391 0.210714 22.5304 0 22 0ZM14 16C14 16.2652 13.8946 16.5196 13.7071 16.7071C13.5196 16.8946 13.2652 17 13 17C12.7348 17 12.4804 16.8946 12.2929 16.7071C12.1054 16.5196 12 16.2652 12 16V12H5V16C5 16.2652 4.89464 16.5196 4.70711 16.7071C4.51957 16.8946 4.26522 17 4 17C3.73478 17 3.48043 16.8946 3.29289 16.7071C3.10536 16.5196 3 16.2652 3 16V6C3 5.73478 3.10536 5.48043 3.29289 5.29289C3.48043 5.10536 3.73478 5 4 5C4.26522 5 4.51957 5.10536 4.70711 5.29289C4.89464 5.48043 5 5.73478 5 6V10H12V6C12 5.73478 12.1054 5.48043 12.2929 5.29289C12.4804 5.10536 12.7348 5 13 5C13.2652 5 13.5196 5.10536 13.7071 5.29289C13.8946 5.48043 14 5.73478 14 6V16ZM21 18C21 18.2652 20.8946 18.5196 20.7071 18.7071C20.5196 18.8946 20.2652 19 20 19C19.7348 19 19.4804 18.8946 19.2929 18.7071C19.1054 18.5196 19 18.2652 19 18V9.875L17.555 10.8387C17.4457 10.9116 17.3231 10.9623 17.1942 10.9878C17.0653 11.0133 16.9326 11.0131 16.8038 10.9874C16.6749 10.9616 16.5524 10.9107 16.4433 10.8376C16.3341 10.7645 16.2404 10.6706 16.1675 10.5612C16.0946 10.4519 16.044 10.3293 16.0185 10.2004C15.993 10.0715 15.9931 9.93887 16.0189 9.81003C16.0447 9.68119 16.0956 9.55868 16.1687 9.44951C16.2418 9.34034 16.3357 9.24663 16.445 9.17375L19.445 7.17375C19.5952 7.07354 19.7697 7.01586 19.9501 7.00684C20.1304 6.99782 20.3098 7.03779 20.4692 7.12251C20.6287 7.20723 20.7622 7.33354 20.8557 7.48804C20.9491 7.64253 20.999 7.81945 21 8V18Z"
fill="currentColor"
/>
</svg>
</div>
<div onClick={setHeading(2)}>
<svg
width="20"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585786C0.210714 0.960859 0 1.46957 0 2V22C0 22.5304 0.210714 23.0391 0.585786 23.4142C0.960859 23.7893 1.46957 24 2 24H22C22.5304 24 23.0391 23.7893 23.4142 23.4142C23.7893 23.0391 24 22.5304 24 22V2C24 1.46957 23.7893 0.960859 23.4142 0.585786C23.0391 0.210714 22.5304 0 22 0ZM12 16C12 16.2652 11.8946 16.5196 11.7071 16.7071C11.5196 16.8946 11.2652 17 11 17C10.7348 17 10.4804 16.8946 10.2929 16.7071C10.1054 16.5196 10 16.2652 10 16V12H5V16C5 16.2652 4.89464 16.5196 4.70711 16.7071C4.51957 16.8946 4.26522 17 4 17C3.73478 17 3.48043 16.8946 3.29289 16.7071C3.10536 16.5196 3 16.2652 3 16V6C3 5.73478 3.10536 5.48043 3.29289 5.29289C3.48043 5.10536 3.73478 5 4 5C4.26522 5 4.51957 5.10536 4.70711 5.29289C4.89464 5.48043 5 5.73478 5 6V10H10V6C10 5.73478 10.1054 5.48043 10.2929 5.29289C10.4804 5.10536 10.7348 5 11 5C11.2652 5 11.5196 5.10536 11.7071 5.29289C11.8946 5.48043 12 5.73478 12 6V16ZM20 19H15C14.8143 19 14.6322 18.9483 14.4743 18.8507C14.3163 18.753 14.1886 18.6133 14.1056 18.4472C14.0225 18.2811 13.9874 18.0952 14.004 17.9102C14.0207 17.7252 14.0886 17.5486 14.2 17.4L18.7 11.4C18.8229 11.2432 18.9133 11.0634 18.966 10.8713C19.0187 10.6791 19.0325 10.4784 19.0068 10.2808C18.981 10.0832 18.9161 9.89275 18.816 9.72052C18.7158 9.54829 18.5823 9.39774 18.4233 9.27768C18.2642 9.15761 18.0829 9.07042 17.8898 9.02121C17.6968 8.972 17.4958 8.96175 17.2987 8.99106C17.1016 9.02037 16.9124 9.08865 16.742 9.19191C16.5716 9.29518 16.4234 9.43136 16.3062 9.5925C16.1576 9.76796 16.0477 9.97286 15.9837 10.1938C15.9588 10.3235 15.9083 10.4471 15.8353 10.5572C15.7623 10.6674 15.6682 10.762 15.5584 10.8355C15.4486 10.9091 15.3253 10.9601 15.1956 10.9858C15.066 11.0114 14.9325 11.011 14.803 10.9848C14.6735 10.9585 14.5505 10.9068 14.441 10.8327C14.3316 10.7586 14.238 10.6636 14.1655 10.553C14.093 10.4425 14.0432 10.3187 14.0189 10.1888C13.9945 10.0589 13.9962 9.9255 14.0238 9.79625C14.129 9.27928 14.349 8.79256 14.6676 8.37206C14.9862 7.95155 15.3952 7.60801 15.8644 7.36682C16.3336 7.12562 16.851 6.99294 17.3784 6.97858C17.9058 6.96422 18.4296 7.06854 18.9113 7.28384C19.3929 7.49914 19.82 7.8199 20.161 8.22245C20.502 8.62499 20.7482 9.09901 20.8814 9.60948C21.0146 10.12 21.0314 10.6538 20.9306 11.1717C20.8297 11.6895 20.6138 12.1781 20.2987 12.6012L17 17H20C20.2652 17 20.5196 17.1054 20.7071 17.2929C20.8946 17.4804 21 17.7348 21 18C21 18.2652 20.8946 18.5196 20.7071 18.7071C20.5196 18.8946 20.2652 19 20 19Z"
fill="currentColor"
/>
</svg>
</div>
<div onClick={setHeading(3)}>
<svg
width="20"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585786C0.210714 0.960859 0 1.46957 0 2V22C0 22.5304 0.210714 23.0391 0.585786 23.4142C0.960859 23.7893 1.46957 24 2 24H22C22.5304 24 23.0391 23.7893 23.4142 23.4142C23.7893 23.0391 24 22.5304 24 22V2C24 1.46957 23.7893 0.960859 23.4142 0.585786C23.0391 0.210714 22.5304 0 22 0ZM12 16C12 16.2652 11.8946 16.5196 11.7071 16.7071C11.5196 16.8946 11.2652 17 11 17C10.7348 17 10.4804 16.8946 10.2929 16.7071C10.1054 16.5196 10 16.2652 10 16V12H5V16C5 16.2652 4.89464 16.5196 4.70711 16.7071C4.51957 16.8946 4.26522 17 4 17C3.73478 17 3.48043 16.8946 3.29289 16.7071C3.10536 16.5196 3 16.2652 3 16V6C3 5.73478 3.10536 5.48043 3.29289 5.29289C3.48043 5.10536 3.73478 5 4 5C4.26522 5 4.51957 5.10536 4.70711 5.29289C4.89464 5.48043 5 5.73478 5 6V10H10V6C10 5.73478 10.1054 5.48043 10.2929 5.29289C10.4804 5.10536 10.7348 5 11 5C11.2652 5 11.5196 5.10536 11.7071 5.29289C11.8946 5.48043 12 5.73478 12 6V16ZM17 19C16.0158 19.0002 15.0661 18.6374 14.3325 17.9813C14.1349 17.8042 14.0157 17.5559 14.0012 17.291C13.9867 17.0262 14.078 16.7663 14.255 16.5688C14.432 16.3712 14.6803 16.252 14.9452 16.2375C15.2101 16.2229 15.4699 16.3142 15.6675 16.4912C15.9092 16.7073 16.1998 16.8613 16.5143 16.9401C16.8287 17.0188 17.1576 17.02 17.4726 16.9434C17.7876 16.8669 18.0793 16.7149 18.3225 16.5006C18.5657 16.2862 18.7532 16.016 18.8688 15.7132C18.9844 15.4103 19.0246 15.0839 18.986 14.762C18.9474 14.4402 18.8312 14.1325 18.6473 13.8655C18.4635 13.5985 18.2174 13.3803 17.9305 13.2295C17.6435 13.0787 17.3242 13 17 13C16.8143 13 16.6322 12.9483 16.4743 12.8507C16.3163 12.753 16.1886 12.6133 16.1056 12.4472C16.0225 12.2811 15.9874 12.0952 16.004 11.9102C16.0207 11.7252 16.0886 11.5486 16.2 11.4L18 9H15C14.7348 9 14.4804 8.89464 14.2929 8.70711C14.1054 8.51957 14 8.26522 14 8C14 7.73478 14.1054 7.48043 14.2929 7.29289C14.4804 7.10536 14.7348 7 15 7H20C20.1857 7 20.3678 7.05171 20.5257 7.14935C20.6837 7.24698 20.8114 7.38668 20.8944 7.55279C20.9775 7.71889 21.0126 7.90484 20.996 8.08981C20.9793 8.27477 20.9114 8.45143 20.8 8.6L18.7113 11.385C19.5321 11.7738 20.1962 12.4304 20.5943 13.2468C20.9924 14.0632 21.1008 14.9908 20.9017 15.877C20.7025 16.7632 20.2077 17.5552 19.4986 18.1228C18.7895 18.6904 17.9083 18.9998 17 19Z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
);
};

View File

@ -166,7 +166,6 @@ export const ManageModal: FC<AddEditModalProps> = (props) => {
return;
}
console.log(checkAllValid);
for (const item of checkAllValid) {
if (item.valid === false) {
toaster.show('Some fields are not valid', 'warning');

View File

@ -12,55 +12,10 @@ import { SelectOrganization } from '@gitroom/frontend/components/new-launch/prov
import { DevtoTags } from '@gitroom/frontend/components/new-launch/providers/devto/devto.tags';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import { Canonical } from '@gitroom/react/form/canonical';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const DevtoPreview: FC = () => {
const { value } = useIntegration();
const settings = useSettings();
const image = useMediaDirectory();
const [coverPicture, title, tags] = settings.watch([
'main_image',
'title',
'tags',
]);
return (
<div
className={clsx(
'font-[800] flex h-[1000px] w-[699.8px] rounded-[10px] bg-customColor32 overflow-hidden overflow-y-auto flex-col gap-[32px]'
)}
>
{!!coverPicture?.path && (
<div className="h-[338.672px]">
<img
className="object-cover w-full h-full"
src={image.set(coverPicture.path)}
alt="cover_picture"
/>
</div>
)}
<div className="px-[60px]">
<div className="text-[48px] leading-[60px] mb-[8px]">{title}</div>
<div className="flex gap-[16px]">
{tags?.map((p: any) => (
<div key={p.label}>#{p.label}</div>
))}
</div>
</div>
<div className="px-[60px]">
<MDEditor.Markdown
style={{
whiteSpace: 'pre-wrap',
}}
skipHtml={true}
source={value.map((p) => p.content).join('\n')}
/>
</div>
</div>
);
};
const DevtoSettings: FC = () => {
const form = useSettings();
const { date } = useIntegration();

View File

@ -13,55 +13,9 @@ import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/provid
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import { MediaComponent } from '@gitroom/frontend/components/media/media.component';
import { Canonical } from '@gitroom/react/form/canonical';
const HashnodePreview: FC = () => {
const { value } = useIntegration();
const settings = useSettings();
const image = useMediaDirectory();
const [coverPicture, title, subtitle] = settings.watch([
'main_image',
'title',
'subtitle',
]);
return (
<div
className={clsx(
'text-center text-black flex h-[1000px] w-[699.8px] rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[32px]'
)}
>
{!!coverPicture?.path && (
<div className="h-[338.672px]">
<img
className="object-cover w-full h-full"
src={image.set(coverPicture.path)}
alt="cover_picture"
/>
</div>
)}
<div className="px-[60px]">
<div className="font-[800] text-[48px] leading-[60px] mb-[8px]">
{title}
</div>
<div className="font-[400] text-[30px] leading-[60px] mb-[8px] text-customColor34">
{subtitle}
</div>
</div>
<div className="px-[60px] text-start">
<MDEditor.Markdown
style={{
whiteSpace: 'pre-wrap',
color: 'black',
}}
skipHtml={true}
source={value.map((p) => p.content).join('\n')}
/>
</div>
</div>
);
};
const HashnodeSettings: FC = () => {
const form = useSettings();
const { date } = useIntegration();

View File

@ -11,40 +11,8 @@ import { MediumPublications } from '@gitroom/frontend/components/new-launch/prov
import { MediumTags } from '@gitroom/frontend/components/new-launch/providers/medium/medium.tags';
import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto';
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
import clsx from 'clsx';
import MDEditor from '@uiw/react-md-editor';
import { Canonical } from '@gitroom/react/form/canonical';
import interClass from '@gitroom/react/helpers/inter.font';
const MediumPreview: FC = () => {
const { value } = useIntegration();
const settings = useSettings();
const [title, subtitle] = settings.watch(['title', 'subtitle']);
return (
<div
className={clsx(
`font-[800] flex h-[1000px] w-[699.8px] text-customColor35 ${interClass} rounded-[10px] bg-white overflow-hidden overflow-y-auto flex-col gap-[56px]`
)}
>
<div className="px-[60px] pt-[20px]">
<div className="text-[48px] leading-[60px] mb-[8px]">{title}</div>
<div className="text-[22px] font-[400] text-customColor36">
{subtitle}
</div>
</div>
<div className="px-[60px]">
<MDEditor.Markdown
style={{
whiteSpace: 'pre-wrap',
color: '#242424',
}}
skipHtml={true}
source={value.map((p) => p.content).join('\n')}
/>
</div>
</div>
);
};
const MediumSettings: FC = () => {
const form = useSettings();
const { date } = useIntegration();

View File

@ -17,7 +17,6 @@ import {
import clsx from 'clsx';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import MDEditor from '@uiw/react-md-editor';
import interClass from '@gitroom/react/helpers/inter.font';
import Image from 'next/image';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
@ -38,14 +37,12 @@ const RenderRedditComponent: FC<{
switch (type) {
case 'self':
return (
<MDEditor.Markdown
<div
dangerouslySetInnerHTML={{ __html: firstPost?.content }}
style={{
whiteSpace: 'pre-wrap',
fontSize: '14px',
}}
skipHtml={true}
disallowedElements={['img']}
source={firstPost?.content}
/>
);
case 'link':
@ -143,13 +140,11 @@ const RedditPreview: FC = (props) => {
<div className="text-[14px] font-[600]">
{integration?.name}
</div>
<MDEditor.Markdown
<div
dangerouslySetInnerHTML={{ __html: p.text }}
style={{
whiteSpace: 'pre-wrap',
}}
skipHtml={true}
source={p.text}
disallowedElements={['img']}
/>
</div>
</div>

View File

@ -3,13 +3,6 @@
import { EventEmitter } from 'events';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import {
executeCommand,
ExecuteState,
ICommand,
selectWord,
TextAreaTextApi,
} from '@uiw/react-md-editor';
import dayjs from 'dayjs';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
@ -211,45 +204,3 @@ export const PostSelector: FC<{
</>
);
};
export const postSelector = (date: dayjs.Dayjs): ICommand => ({
name: 'postselector',
keyCommand: 'postselector',
shortcuts: 'ctrlcmd+p',
prefix: '(post:',
suffix: ')',
buttonProps: {
'aria-label': 'Add Post Url',
title: 'Add Post Url',
},
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
viewBox="0 0 32 32"
fill="currentColor"
>
<path
d="M27.4602 14.6576C27.4039 14.4173 27.2893 14.1947 27.1264 14.0093C26.9635 13.824 26.7575 13.6817 26.5264 13.5951L19.7214 11.0438L21.4714 2.2938C21.5354 1.97378 21.4933 1.64163 21.3514 1.34773C21.2095 1.05382 20.9757 0.814201 20.6854 0.665207C20.395 0.516214 20.064 0.465978 19.7425 0.52212C19.421 0.578262 19.1266 0.737718 18.9039 0.976302L4.90393 15.9763C4.73549 16.1566 4.61413 16.3756 4.55059 16.614C4.48705 16.8525 4.4833 17.1028 4.53968 17.343C4.59605 17.5832 4.7108 17.8058 4.87377 17.9911C5.03673 18.1763 5.24287 18.3185 5.47393 18.4051L12.2789 20.9563L10.5289 29.7063C10.465 30.0263 10.5071 30.3585 10.649 30.6524C10.7908 30.9463 11.0247 31.1859 11.315 31.3349C11.6054 31.4839 11.9364 31.5341 12.2579 31.478C12.5794 31.4218 12.8738 31.2624 13.0964 31.0238L27.0964 16.0238C27.2647 15.8435 27.3859 15.6245 27.4494 15.3862C27.5128 15.1479 27.5165 14.8976 27.4602 14.6576ZM14.5064 25.1163L15.4714 20.2938C15.5412 19.9446 15.4845 19.5819 15.3113 19.2706C15.1382 18.9594 14.86 18.7199 14.5264 18.5951L8.62518 16.3838L17.4914 6.8838L16.5264 11.7063C16.4566 12.0555 16.5134 12.4182 16.6865 12.7295C16.8597 13.0407 17.1379 13.2802 17.4714 13.4051L23.3752 15.6163L14.5064 25.1163Z"
fill="currentColor"
/>
</svg>
),
execute: async (state: ExecuteState, api: TextAreaTextApi) => {
const newSelectionRange = selectWord({
text: state.text,
selection: state.selection,
prefix: state.command.prefix!,
suffix: state.command.suffix,
});
const state1 = api.setSelectionRange(newSelectionRange);
const media = await showPostSelector(date);
executeCommand({
api,
selectedText: state1.selectedText,
selection: state.selection,
prefix: media,
suffix: '',
});
},
});

View File

@ -132,15 +132,20 @@ const underlineMap = {
};
export const stripHtmlValidation = (
type: 'normal' | 'markdown' | 'html',
type: 'plain' | 'none' | 'normal' | 'markdown' | 'html',
value: string,
replaceBold = false
replaceBold = false,
none = false
): string => {
if (type === 'plain') {
return value;
}
if (type === 'markdown') {
return NodeHtmlMarkdown.translate(value);
}
if (value.indexOf('<p>') === -1) {
if (value.indexOf('<p>') === -1 && !none) {
return value;
}
@ -150,15 +155,22 @@ export const stripHtmlValidation = (
.replace(/<p[^>]*>/gi, '\n')
.replace(/<\/p>/gi, '');
if (none) {
return striptags(html);
}
if (replaceBold) {
return striptags(convertLinkedinMention(convertToAscii(html)), [
'ul',
'li',
'h1',
'h2',
'h3',
]);
}
// Strip all other tags
return striptags(html, ['ul', 'li']);
return striptags(html, ['ul', 'li', 'h1', 'h2', 'h3']);
};
export const convertLinkedinMention = (value: string) => {

View File

@ -81,6 +81,7 @@
"@tailwindcss/postcss": "^4.1.7",
"@tiptap/extension-bold": "^3.0.6",
"@tiptap/extension-document": "^3.0.6",
"@tiptap/extension-heading": "^3.0.7",
"@tiptap/extension-history": "^3.0.7",
"@tiptap/extension-list": "^3.0.7",
"@tiptap/extension-paragraph": "^3.0.6",

View File

@ -120,6 +120,9 @@ importers:
'@tiptap/extension-document':
specifier: ^3.0.6
version: 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-heading':
specifier: ^3.0.7
version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-history':
specifier: ^3.0.7
version: 3.0.7(@tiptap/extensions@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
@ -5493,10 +5496,10 @@ packages:
peerDependencies:
'@tiptap/core': ^3.0.6
'@tiptap/extension-heading@3.0.6':
resolution: {integrity: sha512-umcsnc4IEacQjlnl7d/IaiFsVA5xG993VvnUG5fK08TOuy0yrliOqXsFGp4czGnRLBEh2zfXMMK+vnm2fuk5zw==}
'@tiptap/extension-heading@3.0.7':
resolution: {integrity: sha512-uS7fFcilFuzKEvhUgndELqlGweD+nZeLOb6oqUE5hM49vECjM7qVjVQnlhV+MH2W1w8eD08cn1lu6lDxaMOe5w==}
peerDependencies:
'@tiptap/core': ^3.0.6
'@tiptap/core': ^3.0.7
'@tiptap/extension-history@3.0.7':
resolution: {integrity: sha512-F+zjS7Wz53sNCWh3KqSAug4/COgxs060tR9up0OXjw7iB3gJz6JMNpGaWmYF5WdOsLxph7FGRMb/Mr5keCqVDA==}
@ -21208,7 +21211,7 @@ snapshots:
dependencies:
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
'@tiptap/extension-heading@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))':
'@tiptap/extension-heading@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))':
dependencies:
'@tiptap/core': 3.0.6(@tiptap/pm@3.0.6)
@ -21317,7 +21320,7 @@ snapshots:
'@tiptap/extension-dropcursor': 3.0.6(@tiptap/extensions@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
'@tiptap/extension-gapcursor': 3.0.6(@tiptap/extensions@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))
'@tiptap/extension-hard-break': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-heading': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-heading': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-horizontal-rule': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)
'@tiptap/extension-italic': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))
'@tiptap/extension-link': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)