Skip to content

Commit 0cfbd71

Browse files
rajat1saxenaRajat Saxena
andauthored
Tiptap based editor (#683)
* migrated to tiptap * Fixed deps * Added AGENTS.md * Nextjs 16 (#667) * Next 16 * Turned off ignoreBuildErrors * v0.65.1 * Text renderer based on page-primitives; Table block for editor and renderer; table-of-content component; editor ui tweaks; * bubble menu z index * Lint fix * Fixing codemirror rendering * Merged main * Removed hack for theme switching * Reverted the update logic (#672) Co-authored-by: Rajat <hi@rajatsaxena.dev> * deleting block media on block delete * Added error handler to text editor; added highlight extension; * Code mirror compatibility extension for tiptap --------- Co-authored-by: Rajat Saxena <hi@rajatsaxena.dev>
1 parent d891fa9 commit 0cfbd71

76 files changed

Lines changed: 2948 additions & 3443 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
4.49 MB
Loading

apps/docs/src/pages/en/website/blocks.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ layout: ../../../layouts/MainLayout.astro
66

77
Every page in CourseLit is made up of various blocks, stacked in a top-to-bottom fashion. Each block serves a unique purpose and can be customized.
88

9-
The following screenshot shows [Header](/en/pages/header), [Rich Text](/en/pages/banner), [Hero](/en/pages/content), and [Grid](/en/pages/grid) blocks (top to bottom) in action. Different blocks are highlighted in different colors.
9+
The following screenshot shows [Header](/en/website/blocks#header), [Rich Text](/en/website/blocks#rich-text), [Hero](/en/website/blocks#hero), and [Grid](/en/website/blocks#grid) blocks (top to bottom) in action. Different blocks are highlighted in different colors.
1010

1111
![CourseLit page blocks](/assets/pages/page-builder-blocks.png)
1212

@@ -36,32 +36,40 @@ You will also see the newly added link on the header itself.
3636
3. Click on the pencil icon against the newly added link to edit it as shown above.
3737
4. Change the label (displayed as text on the header block) and the URL (where the user should be taken upon clicking the label on the header) and click `Done` to save.
3838
![Header edit link](/assets/pages/header-edit-link.png)
39-
</details>
39+
</details>
4040

4141
### [Rich Text](#rich-text)
4242

4343
<details>
4444
<summary>Expand to see Rich Text block details</summary>
4545

46-
The rich text block can be used to add text blocks containing elements like hyperlinks, etc.
46+
The rich text block uses the same text editor available elsewhere on the platform. It supports all functionality that does not require a toolbar, as the toolbar is hidden in this block.
4747

48-
#### Making text bold/italic/underline
48+
#### Keyboard shortcuts
4949

5050
1. Select the text.
51-
2. To make the selected text bold, press <kbd>Ctrl+B</kbd>; to make it italic, press <kbd>Ctrl+I</kbd>; and for underline, press <kbd>Ctrl+U</kbd>.
52-
53-
You can also use the floating controls to do the same as shown below.
51+
2. Use the following shortcuts to format it:
52+
53+
- **Bold**: <kbd>Ctrl+B</kbd>
54+
- **Italic**: <kbd>Ctrl+I</kbd>
55+
- **Underline**: <kbd>Ctrl+U</kbd>
56+
- **Strikethrough**: <kbd>Ctrl+Shift+S</kbd>
57+
- **Undo**: <kbd>Ctrl+Z</kbd>
58+
- **Redo**: <kbd>Ctrl+Shift+Z</kbd>
59+
- **Paste**: <kbd>Ctrl+V</kbd>
60+
- **Ordered list**: <kbd>Ctrl+Shift+7</kbd>
61+
- **Bulleted list**: <kbd>Ctrl+Shift+8</kbd>
62+
- **Highlight**: <kbd>Ctrl+Shift+H</kbd> (or type `==two equal signs==`)
5463

5564
![Stylised text](/assets/pages/rich-text-styling.gif)
5665

5766
#### Creating hyperlinks
5867

5968
1. Select the text.
60-
> Double-clicking the text to select won't work due to a bug. We are working on it.
61-
2. Click on the floating `link` button to reveal a popup text input.
62-
3. In the popup text input, enter the URL as shown below.
63-
![Create a hyperlink in rich text block](/assets/pages/rich-text-create-hyperlink.gif)
64-
</details>
69+
2. Click on the floating `link` icon to reveal a text input.
70+
3. In the popup text input, enter the URL as shown below and press <kbd>Enter</kbd>.
71+
![Create a hyperlink in rich text block](/assets/pages/courselit-text-editor-create-links.gif)
72+
</details>
6573

6674
### [Hero](#hero)
6775

@@ -87,7 +95,7 @@ Following is how it looks on a page.
8795
4. In the button action, enter the URL the user should be taken to upon clicking.
8896
a. If the URL is from your own school, use its relative form, i.e., `/courses`.
8997
b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`.
90-
</details>
98+
</details>
9199

92100
### [Grid](#grid)
93101

@@ -132,7 +140,7 @@ A grid block comes in handy when you want to show some sort of list, for example
132140
4. In the button action, enter the URL the user should be taken to upon clicking.
133141
a. If the URL is from your own school, use its relative form, i.e., `/courses`.
134142
b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`.
135-
</details>
143+
</details>
136144

137145
### [Featured](#featured)
138146

@@ -268,7 +276,7 @@ In the `Design` panel, you can customize:
268276
- Maximum width
269277
- Vertical padding
270278
- Social media links (Facebook, Twitter, Instagram, LinkedIn, YouTube, Discord, GitHub)
271-
</details>
279+
</details>
272280

273281
## [Shared blocks](#shared-blocks)
274282

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
"use client";
22

3-
import { TextRenderer } from "@courselit/components-library";
3+
import { TextRenderer } from "@courselit/page-blocks";
4+
import { ThemeStyle } from "@courselit/page-models";
5+
import { TableOfContent } from "@components/table-of-content";
6+
import WidgetErrorBoundary from "@components/public/base-layout/template/widget-error-boundary";
47

5-
export default function ClientSideTextRenderer({ json }: { json: any }) {
6-
return <TextRenderer json={json} showTableOfContent={true} />;
8+
export default function ClientSideTextRenderer({
9+
json,
10+
theme,
11+
}: {
12+
json: any;
13+
theme: ThemeStyle;
14+
}) {
15+
return (
16+
<div className="flex flex-col gap-4">
17+
<TableOfContent json={json} theme={theme} />
18+
<WidgetErrorBoundary widgetName="text-editor">
19+
<TextRenderer json={json} theme={theme} />
20+
</WidgetErrorBoundary>
21+
</div>
22+
);
723
}

apps/web/app/(with-contexts)/(with-layout)/blog/[slug]/[id]/page.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Course } from "@courselit/common-models";
2-
import { Caption, Header1, Section, Text1 } from "@courselit/page-primitives";
2+
import { Caption, Header1, Section } from "@courselit/page-primitives";
33
import { formattedLocaleDate, getFullSiteSetup } from "@ui-lib/utils";
44
import { getAddressFromHeaders } from "@/app/actions";
55
import { headers } from "next/headers";
@@ -96,11 +96,10 @@ export default async function ProductPage(props: {
9696
</div>
9797
)}
9898
{product?.description && (
99-
<Text1 theme={theme.theme} component="span">
100-
<ClientSideTextRenderer
101-
json={JSON.parse(product.description)}
102-
/>
103-
</Text1>
99+
<ClientSideTextRenderer
100+
json={JSON.parse(product.description)}
101+
theme={theme.theme}
102+
/>
104103
)}
105104
</div>
106105
</Section>

apps/web/app/(with-contexts)/course/[slug]/[id]/page.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@ import {
1414
Link,
1515
Button2,
1616
getSymbolFromCurrency,
17-
TextRenderer,
18-
TextEditorEmptyDoc,
1917
Image,
2018
} from "@courselit/components-library";
19+
import { TextRenderer } from "@courselit/page-blocks";
20+
import { TableOfContent } from "@components/table-of-content";
2121
import {
2222
AddressContext,
2323
ProfileContext,
2424
SiteInfoContext,
25+
ThemeContext,
2526
} from "@components/contexts";
2627
import { getProduct } from "./helpers";
2728
import { getUserProfile } from "@/app/(with-contexts)/helpers";
2829
import { BadgeCheck } from "lucide-react";
30+
import { emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor";
31+
import WidgetErrorBoundary from "@components/public/base-layout/template/widget-error-boundary";
2932
const { permissions } = UIConstants;
3033

3134
export default function ProductPage(props: {
@@ -38,6 +41,7 @@ export default function ProductPage(props: {
3841
const siteInfo = useContext(SiteInfoContext);
3942
const address = useContext(AddressContext);
4043
const [progress, setProgress] = useState<any>(null);
44+
const { theme } = useContext(ThemeContext);
4145

4246
useEffect(() => {
4347
if (id) {
@@ -68,6 +72,10 @@ export default function ProductPage(props: {
6872
return null;
6973
}
7074

75+
const descriptionJson = product.description
76+
? JSON.parse(product.description)
77+
: TextEditorEmptyDoc;
78+
7179
return (
7280
<div className="flex flex-col pb-[100px] lg:max-w-[40rem] xl:max-w-[48rem] mx-auto">
7381
<h1 className="text-4xl font-semibold mb-8">{product.title}</h1>
@@ -118,14 +126,18 @@ export default function ProductPage(props: {
118126
</div>
119127
)}
120128
<div className="overflow-hidden min-h-[360px]">
121-
<TextRenderer
122-
json={
123-
product.description
124-
? JSON.parse(product.description)
125-
: TextEditorEmptyDoc
126-
}
127-
showTableOfContent={true}
128-
/>
129+
<div className="flex flex-col gap-4">
130+
<TableOfContent
131+
json={descriptionJson}
132+
theme={theme.theme}
133+
/>
134+
<WidgetErrorBoundary widgetName="text-editor">
135+
<TextRenderer
136+
json={descriptionJson}
137+
theme={theme.theme}
138+
/>
139+
</WidgetErrorBoundary>
140+
</div>
129141
</div>
130142
{isEnrolled(product.courseId, profile as Profile) && (
131143
<div className="self-end">

apps/web/app/(with-contexts)/dashboard/(sidebar)/community/[id]/manage/page.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
"use client";
22

33
import DashboardContent from "@components/admin/dashboard-content";
4-
import {
5-
AddressContext,
6-
ProfileContext,
7-
SiteInfoContext,
8-
} from "@components/contexts";
4+
import { AddressContext, ProfileContext } from "@components/contexts";
95
import {
106
COMMUNITY_HEADER,
117
COMMUNITY_SETTINGS,
@@ -31,8 +27,6 @@ import {
3127
Image,
3228
Link,
3329
MediaSelector,
34-
TextEditor,
35-
TextEditorEmptyDoc,
3630
useToast,
3731
} from "@courselit/components-library";
3832
import { Separator } from "@components/ui/separator";
@@ -72,6 +66,7 @@ import { Input } from "@/components/ui/input";
7266
import { redirect, useRouter } from "next/navigation";
7367
import { useMembership } from "@/hooks/use-membership";
7468
import { useGraphQLFetch } from "@/hooks/use-graphql-fetch";
69+
import { Editor, emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor";
7570
const { PaymentPlanType: paymentPlanType, MembershipEntityType } = Constants;
7671

7772
export default function Page(props: {
@@ -90,7 +85,6 @@ export default function Page(props: {
9085
];
9186
const { profile } = useContext(ProfileContext);
9287
const address = useContext(AddressContext);
93-
const siteinfo = useContext(SiteInfoContext);
9488

9589
const [name, setName] = useState("");
9690
const [enabled, setEnabled] = useState(false);
@@ -565,7 +559,7 @@ export default function Page(props: {
565559
/>
566560
<div>
567561
<h2 className="font-semibold">Description</h2>
568-
<TextEditor
562+
<Editor
569563
initialContent={description}
570564
onChange={(state: any) => setDescription(state)}
571565
showToolbar={false}

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,7 @@ import {
5151
TextEditorContent,
5252
UIConstants,
5353
} from "@courselit/common-models";
54-
import {
55-
MediaSelector,
56-
TextEditor,
57-
TextEditorEmptyDoc,
58-
useToast,
59-
} from "@courselit/components-library";
54+
import { MediaSelector, useToast } from "@courselit/components-library";
6055
import {
6156
MIMETYPE_VIDEO,
6257
MIMETYPE_AUDIO,
@@ -67,6 +62,7 @@ import { QuizBuilder } from "@components/admin/products/quiz-builder";
6762
import { isTextEditorNonEmpty, truncate } from "@ui-lib/utils";
6863
import { Skeleton } from "@/components/ui/skeleton";
6964
import { Separator } from "@components/ui/separator";
65+
import { Editor, emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor";
7066

7167
const { permissions } = UIConstants;
7268

@@ -214,7 +210,7 @@ export default function LessonPage() {
214210
case Constants.LessonType.TEXT:
215211
return (
216212
<div className="space-y-2">
217-
<TextEditor
213+
<Editor
218214
initialContent={textContent}
219215
refresh={refresh}
220216
onChange={(state: any) => {

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/components/product-details.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { Button } from "@/components/ui/button";
55
import { Input } from "@/components/ui/input";
66
import { Label } from "@/components/ui/label";
77
import { Separator } from "@/components/ui/separator";
8-
import {
9-
TextEditor,
10-
TextEditorEmptyDoc,
11-
useToast,
12-
} from "@courselit/components-library";
8+
import { useToast } from "@courselit/components-library";
139
import { AddressContext } from "@components/contexts";
1410
import {
1511
APP_MESSAGE_COURSE_SAVED,
@@ -20,6 +16,7 @@ import {
2016
} from "@ui-config/strings";
2117
import { useGraphQLFetch } from "@/hooks/use-graphql-fetch";
2218
import { Save, Loader2 } from "lucide-react";
19+
import { Editor, emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor";
2320

2421
const MUTATION_UPDATE_BASIC_DETAILS = `
2522
mutation UpdateBasicDetails($courseId: String!, $title: String!, $description: String!) {
@@ -131,7 +128,7 @@ export default function ProductDetails({ product }: ProductDetailsProps) {
131128
Description
132129
</Label>
133130

134-
<TextEditor
131+
<Editor
135132
initialContent={formData.description}
136133
onChange={(state: any) => {
137134
handleInputChange({

apps/web/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import "remirror/styles/all.css";
21
import "@courselit/page-blocks/styles.css";
32
import "@courselit/components-library/styles.css";
43
import "@courselit/page-primitives/styles.css";
4+
import "@courselit/text-editor/styles.css";
55
import "../styles/globals.css";
66
import type { Metadata } from "next";
77
import { headers } from "next/headers";

apps/web/components/admin/blogs/editor/details.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React, { FormEvent, useContext, useEffect, useState } from "react";
22
import {
33
MediaSelector,
4-
TextEditor,
5-
TextEditorEmptyDoc,
64
Form,
75
FormField,
86
Button,
@@ -23,6 +21,7 @@ import {
2321
import { MIMETYPE_IMAGE } from "@/ui-config/constants";
2422
import { Media, Profile } from "@courselit/common-models";
2523
import { AddressContext, ProfileContext } from "@components/contexts";
24+
import { Editor, emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor";
2625

2726
interface DetailsProps {
2827
id: string;
@@ -145,12 +144,19 @@ export default function Details({ id }: DetailsProps) {
145144
onChange={(e) => setTitle(e.target.value)}
146145
/>
147146
<PageBuilderPropertyHeader label={COURSE_CONTENT_HEADER} />
148-
<TextEditor
147+
<Editor
149148
initialContent={description}
150149
refresh={refreshDetails}
151150
onChange={(state: any) => setDescription(state)}
152151
url={address.backend}
153152
placeholder={TEXT_EDITOR_PLACEHOLDER}
153+
onError={(err: any) => {
154+
toast({
155+
title: TOAST_TITLE_ERROR,
156+
description: err,
157+
variant: "destructive",
158+
});
159+
}}
154160
/>
155161
<div>
156162
<Button type="submit" disabled={loading}>

0 commit comments

Comments
 (0)