Skip to content
This repository has been archived by the owner on Jan 20, 2025. It is now read-only.

Add richt text editor with preview functionality #114

Merged
merged 5 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,724 changes: 1,617 additions & 107 deletions client/package-lock.json

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@
"dependencies": {
"@emotion/react": "^11.10.6",
"@formkit/auto-animate": "^0.7.0",
"@mantine/core": "6.0.19",
"@mantine/dates": "6.0.19",
"@mantine/dropzone": "^6.0.19",
"@mantine/form": "6.0.19",
"@mantine/hooks": "6.0.19",
"@mantine/notifications": "6.0.19",
"@mantine/core": "6.0.21",
"@mantine/dates": "6.0.21",
"@mantine/dropzone": "6.0.21",
"@mantine/form": "6.0.21",
"@mantine/hooks": "6.0.21",
"@mantine/notifications": "6.0.21",
"@mantine/tiptap": "^6.0.21",
"@reduxjs/toolkit": "^1.9.3",
"@tabler/icons-react": "^2.20.0",
"@tiptap/extension-color": "^2.1.11",
"@tiptap/extension-highlight": "^2.1.11",
"@tiptap/extension-link": "^2.1.11",
"@tiptap/extension-subscript": "^2.1.11",
"@tiptap/extension-superscript": "^2.1.11",
"@tiptap/extension-text-align": "^2.1.11",
"@tiptap/extension-text-style": "^2.1.11",
"@tiptap/extension-underline": "^2.1.11",
"@tiptap/pm": "^2.1.11",
"@tiptap/react": "^2.1.11",
"@tiptap/starter-kit": "^2.1.11",
"@types/node": "^16.18.12",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
Expand All @@ -21,7 +33,7 @@
"jwt-decode": "^3.1.2",
"keycloak-js": "^21.1.1",
"lodash.sortby": "^4.7.0",
"mantine-datatable": "2.9.0",
"mantine-datatable": "6.0.0",
"moment": "^2.29.4",
"papaparse": "^5.4.1",
"react": "^18.2.0",
Expand Down
11 changes: 11 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IntroCourseConsole } from './instructor/IntroCourse/IntroCourseConsole'
import type Keycloak from 'keycloak-js'
import { ThesisApplicationsManagementConsole } from './instructor/ThesisApplicationsManagement/ThesisApplicationsManagementConsole'
import { StudentTechnicalDetailsSubmissionPage } from './student/StudentPostKickoffSubmissionPage/StudentTechnicalDetailsSubmissionPage'
import { MailingManagementConsole } from './instructor/MailingManagement/MailingManagementConsole'

export const App = (): JSX.Element => {
const [keycloakValue, setKeycloakValue] = useState<Keycloak>()
Expand Down Expand Up @@ -91,6 +92,16 @@ export const App = (): JSX.Element => {
/>
}
/>
<Route
path='/management/mailing'
element={
<ManagementConsole
child={<MailingManagementConsole />}
permission={['ipraktikum-pm', 'chair-member']}
onKeycloakValueChange={setKeycloakValue}
/>
}
/>
<Route
path='/management'
element={
Expand Down
5 changes: 5 additions & 0 deletions client/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ declare module 'react-beautiful-dnd'
declare module 'react-csv'
declare module 'papaparse'
declare module 'lodash/sortBy'
declare module '@tiptap/extension-highlight'
declare module '@tiptap/extension-underline'
declare module '@tiptap/extension-text-align'
declare module '@tiptap/extension-superscript'
declare module '@tiptap/extension-subscript'
316 changes: 316 additions & 0 deletions client/src/instructor/MailingManagement/MailingManagementConsole.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
import { RichTextEditor, Link } from '@mantine/tiptap'
import { useEditor, BubbleMenu } from '@tiptap/react'
import Highlight from '@tiptap/extension-highlight'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import TextAlign from '@tiptap/extension-text-align'
import Superscript from '@tiptap/extension-superscript'
import SubScript from '@tiptap/extension-subscript'
import {
ActionIcon,
Button,
Card,
Collapse,
ColorPicker,
Group,
Select,
Stack,
Tabs,
Text,
} from '@mantine/core'
import { MailTemplate, fetchMailTemplate, updateMailTemplate } from '../../service/mailingService'
import { useEffect, useState } from 'react'
import { IconEyeCheck, IconHtml, IconInfoCircle } from '@tabler/icons-react'
import {
StudentAndCourseIterationInstructions,
StudentAndCourseIterationAndDeveloperApplicationInstructions,
ThesisApplicationAndStudentAndThesisAdvisorInstructions,
ThesisApplicationAndStudentInstructions,
StudentAndCourseIterationAndCoachApplicationInstructions,
StudentAndCourseIterationAndTutorApplicationInstructions,
fillMockStudentPlaceholders,
fillMockCourseIterationPlaceholders,
fillMockThesisAdvisorPlaceholders,
fillMockThesisApplicationPlaceholders,
fillDeveloperApplicationPlaceholders,
fillMockCoachApplicationPlaceholders,
fillMockTutorApplicationPlaceholders,
} from './components/TemplateInstructions'

export const MailingManagementConsole = (): JSX.Element => {
const [activeTemplate, setActiveTemplate] = useState<string | null>(null)
const [instructionsOpened, setInstructionsOpened] = useState(false)
const [activeTab, setActiveTab] = useState<string | null>('editor')
const [previewContent, setPreviewContent] = useState<string>('')
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
ColorPicker,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: '',
})

useEffect(() => {
void (async () => {
if (activeTemplate) {
const response = await fetchMailTemplate(activeTemplate)
if (response) {
editor?.chain().setContent(response).run()
}
}
})()
}, [editor, activeTemplate])

useEffect(() => {
if (activeTab === 'preview') {
let preview = fillMockStudentPlaceholders(editor?.getHTML() ?? '')
preview = fillMockCourseIterationPlaceholders(preview)
if (
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_CONFIRMATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_CREATED ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_ACCEPTANCE ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_REJECTION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_ACCEPTANCE_NO_ADVISOR
) {
preview = fillMockThesisAdvisorPlaceholders(preview)
preview = fillMockThesisApplicationPlaceholders(preview)
}
if (
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.DEVELOPER_APPLICATION_CONFIRMATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TECHNICAL_DETAILS_SUBMISSION_INVIATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.KICK_OFF_SUBMISSION_INVITATION
) {
preview = fillDeveloperApplicationPlaceholders(preview)
}
if (
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_APPLICATION_ACCEPTANCE ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_APPLICATION_REJECTION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_APPLICATION_CONFIRMATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_INTERVIEW_INVITATION
) {
preview = fillMockCoachApplicationPlaceholders(preview)
}
if (
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_APPLICATION_ACCEPTANCE ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_APPLICATION_REJECTION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_APPLICATION_CONFIRMATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_INTERVIEW_INVITATION
) {
preview = fillMockTutorApplicationPlaceholders(preview)
}
setPreviewContent(preview)
}
}, [activeTab])

return (
<Stack>
<Group style={{ alignItems: 'end' }}>
<Select
style={{ width: '60vw' }}
label='Template'
placeholder='Template'
data={Object.keys(MailTemplate).map((key) => {
return {
label: MailTemplate[key as keyof typeof MailTemplate],
value: key,
}
})}
value={activeTemplate}
onChange={(value) => {
setActiveTab('editor')
setActiveTemplate(value as MailTemplate)
}}
/>
<ActionIcon
size={36}
color='blue'
variant='filled'
onClick={() => {
setInstructionsOpened(!instructionsOpened)
}}
>
<IconInfoCircle />
</ActionIcon>
</Group>
<Collapse in={instructionsOpened}>
<Card>
<Text fz='xs'>
For the below template you can use the following placeholders. Please wrap each accessor
with double curly brackets.
</Text>
{(MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TECHNICAL_DETAILS_SUBMISSION_INVIATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.KICK_OFF_SUBMISSION_INVITATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_INTERVIEW_INVITATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_INTERVIEW_INVITATION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_APPLICATION_ACCEPTANCE ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_APPLICATION_ACCEPTANCE ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_APPLICATION_REJECTION ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_APPLICATION_REJECTION) && (
<StudentAndCourseIterationInstructions />
)}
{MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.DEVELOPER_APPLICATION_CONFIRMATION && (
<StudentAndCourseIterationAndDeveloperApplicationInstructions />
)}
{MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.COACH_APPLICATION_CONFIRMATION && (
<StudentAndCourseIterationAndCoachApplicationInstructions />
)}
{MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.TUTOR_APPLICATION_CONFIRMATION && (
<StudentAndCourseIterationAndTutorApplicationInstructions />
)}
{(MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_ACCEPTANCE_NO_ADVISOR ||
MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_REJECTION) && (
<ThesisApplicationAndStudentInstructions />
)}
{MailTemplate[activeTemplate as keyof typeof MailTemplate] ===
MailTemplate.THESIS_APPLICATION_ACCEPTANCE && (
<ThesisApplicationAndStudentAndThesisAdvisorInstructions />
)}
</Card>
</Collapse>
<Tabs
defaultValue='editor'
value={activeTab}
onTabChange={(value) => {
setActiveTab(value)
}}
>
<Tabs.List>
<Tabs.Tab value='editor' icon={<IconHtml size='0.8rem' />}>
Editor
</Tabs.Tab>
<Tabs.Tab value='preview' icon={<IconEyeCheck size='0.8rem' />}>
Preview
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value='editor' pt='xs'>
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>

<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
<RichTextEditor.H5 />
<RichTextEditor.H6 />
</RichTextEditor.ControlsGroup>

<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>

<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>

<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>

<RichTextEditor.ControlsGroup>
<RichTextEditor.ColorPicker
colors={[
'#25262b',
'#868e96',
'#fa5252',
'#e64980',
'#be4bdb',
'#7950f2',
'#4c6ef5',
'#228be6',
'#15aabf',
'#12b886',
'#40c057',
'#82c91e',
'#fab005',
'#fd7e14',
]}
/>
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
{editor && (
<BubbleMenu editor={editor}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Link />
</RichTextEditor.ControlsGroup>
</BubbleMenu>
)}
<RichTextEditor.Content />
</RichTextEditor>
</Tabs.Panel>
<Tabs.Panel value='preview' pt='xs'>
<Card>
<div dangerouslySetInnerHTML={{ __html: previewContent }} />
</Card>
</Tabs.Panel>
</Tabs>
<Group
position='right'
onClick={() => {
void (async () => {
if (activeTemplate) {
await updateMailTemplate(activeTemplate, editor?.getHTML() ?? '')
}
})()
}}
>
<Button>Save</Button>
</Group>
</Stack>
)
}
Loading