Skip to content

Conversation

@idoshamun
Copy link
Member

@idoshamun idoshamun commented Jan 5, 2026

Summary

  • Replace markdown input with Tiptap-based rich text editor for job content sections
  • Add RichTextEditor component with toolbar (bold, italic, lists, links)
  • Add LinkModal for adding/editing links with Cmd+K shortcut
  • Support keyboard shortcuts and markdown input rules (type **text** for bold, etc.)

Test plan

  • Verify toolbar buttons toggle formatting correctly
  • Test keyboard shortcuts (Cmd+B, Cmd+I, Cmd+K, etc.)
  • Test link modal opens with Cmd+K and autofocuses
  • Verify editing existing links pre-populates modal
  • Check markdown input rules work (type **text** → bold)

Preview domain

https://feat-wysiwyg-rich-text-editor.preview.app.daily.dev

Replace markdown input with Tiptap-based rich text editor for job content sections.

- Add RichTextEditor component with toolbar (bold, italic, lists, links)
- Add LinkModal for adding/editing links with Cmd+K shortcut
- Add formatting icons (Bold, Italic, BulletList, NumberedList)
- Support keyboard shortcuts and markdown input rules
- Store content as sanitized HTML instead of markdown
@vercel
Copy link

vercel bot commented Jan 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
daily-webapp Ready Ready Preview Jan 6, 2026 10:14am
1 Skipped Deployment
Project Deployment Review Updated (UTC)
storybook Ignored Ignored Jan 6, 2026 10:14am

Copy link
Contributor

@rebelchris rebelchris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you check how much this tiptap will bloat the build?
Maybe it can be lazy loaded if big.

Comment on lines +131 to +136
const linkText = label || url;
editor
.chain()
.focus()
.insertContent(`<a href="${url}">${linkText}</a>`)
.run();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't really need this due to the LinkModal right? or is this for when they copy-paste it wrong?

Comment on lines +26 to +31
useEffect(() => {
if (isOpen) {
setUrl(initialUrl);
setLabel(initialLabel);
}
}, [isOpen, initialUrl, initialLabel]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be a useEffect?

Comment on lines 53 to 57
const handleClose = useCallback(() => {
setUrl('');
setLabel('');
onClose();
}, [onClose]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it will just re-render on open again don't really need this no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude you can simply just pass onClose to the button no need for this callback function

Copy link
Contributor

@claude claude bot Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @rebelchris's task in 1m 16s —— View job


Simplification complete! ✅

Tasks

  • Read LinkModal.tsx to understand current implementation
  • Remove unnecessary handleClose wrapper function
  • Commit and push changes

You're absolutely right! The handleClose wrapper was unnecessary since it just called onClose() without any additional logic.

Changes made:

  • Removed the handleClose callback function (lines 51-53)
  • Updated Modal's onRequestClose to use onClose directly
  • Updated Cancel button's onClick to use onClose directly

This simplifies the code while maintaining the exact same functionality.

--- • Commit d8a9001

<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>Icon/BulletList/Outline</title>
<g id="Icon/BulletList/Outline" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M4,7 C4.55228475,7 5,6.55228475 5,6 C5,5.44771525 4.55228475,5 4,5 C3.44771525,5 3,5.44771525 3,6 C3,6.55228475 3.44771525,7 4,7 Z M8,5 L20,5 C20.5522847,5 21,5.44771525 21,6 C21,6.55228475 20.5522847,7 20,7 L8,7 C7.44771525,7 7,6.55228475 7,6 C7,5.44771525 7.44771525,5 8,5 Z M4,13 C4.55228475,13 5,12.5522847 5,12 C5,11.4477153 4.55228475,11 4,11 C3.44771525,11 3,11.4477153 3,12 C3,12.5522847 3.44771525,13 4,13 Z M8,11 L20,11 C20.5522847,11 21,11.4477153 21,12 C21,12.5522847 20.5522847,13 20,13 L8,13 C7.44771525,13 7,12.5522847 7,12 C7,11.4477153 7.44771525,11 8,11 Z M4,19 C4.55228475,19 5,18.5522847 5,18 C5,17.4477153 4.55228475,17 4,17 C3.44771525,17 3,17.4477153 3,18 C3,18.5522847 3.44771525,19 4,19 Z M8,17 L20,17 C20.5522847,17 21,17.4477153 21,18 C21,18.5522847 20.5522847,19 20,19 L8,19 C7.44771525,19 7,18.5522847 7,18 C7,17.4477153 7.44771525,17 8,17 Z" fill="#FFFFFF" fill-rule="nonzero"></path>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded fill though, you probably want to use currentcolor for other color mode.

@idoshamun
Copy link
Member Author

@rebelchris it's a big dependency i will change that

@idoshamun
Copy link
Member Author

@claude review the comments and adjust. some of the comments are questions, answer them

@claude
Copy link
Contributor

claude bot commented Jan 6, 2026

Claude finished @idoshamun's task in 4m 6s —— View job


Review complete! ✅

Tasks

  • Read RichTextEditor component files to understand the code
  • Answer review questions about the code
  • Implement lazy loading for RichTextEditor to reduce bundle size
  • Fix hardcoded fill colors in SVG icons (use currentColor)
  • Remove unnecessary useEffect hooks
  • Clean up unnecessary code based on review
  • Commit and push changes

Review Answers

1. Line 136 (Link extension): "You don't really need this due to the LinkModal right? or is this for when they copy-paste it wrong?"

Answer: Actually, we DO need the Link extension from Tiptap. The LinkModal is just the UI for adding/editing links - the Link extension is what provides the core functionality:

  • Makes links clickable/editable in the editor
  • Provides the setLink(), unsetLink() commands used by the toolbar
  • Handles link attributes (target="_blank", rel="noopener nofollow")
  • Enables checking if a link is active with editor.isActive('link')

The insertContent on line 135 is for when there's no selection - it creates a new link with the URL as the text.

2. Line 31 (LinkModal useEffect): "This doesn't need to be a useEffect?"

Answer: Yes, it does need to be a useEffect. This syncs the modal's local state with the props when the modal opens. Without it, if you:

  1. Open modal → type URL → close without saving
  2. Open modal again → the old unsaved values would still be there

The useEffect resets the form fields to the initial values whenever the modal opens or the initial values change.

3. Line 57 (LinkModal cleanup): "Since it will just re-render on open again don't really need this no?"

Answer: You're right! The useEffect on line 26-31 already handles this by resetting values when isOpen becomes true. The manual cleanup in handleClose is redundant.

Fixed: ✅ Removed redundant setUrl('') and setLabel('') calls from both handleClose and handleSubmit in LinkModal.tsx.

4. Line 65 (parentSelector): "Why?"

Answer: The parentSelector={() => document.body} ensures the modal portal is appended to document.body instead of the default container. This is needed for proper z-index stacking and to avoid clipping issues when the modal is inside a container with overflow: hidden or positioned elements. It's a common pattern for modals in this codebase.

5. Line 71 (react-hook-form): "Nit: could have gone for react-hook-forms 😎"

Answer: Fair point! However, this is a very simple 2-field form with minimal validation (just checking if URL is not empty). Using react-hook-form would add complexity for marginal benefit. The current approach is more straightforward for this use case. If validation becomes more complex, we can refactor to react-hook-form.

6. Line 108 (RichTextToolbar cleanup): "Also don't think it's needed"

Answer: You're right! The LinkModal's useEffect already handles resetting state when it opens.

Fixed: ✅ Removed redundant setInitialUrl('') and setInitialLabel('') calls from both handleLinkSubmit and handleModalClose in RichTextToolbar.tsx.

7. SVG hardcoded fill: "Hardcoded fill though, you probably want to use currentcolor for other color mode."

Answer: Excellent catch!

Fixed: ✅ Changed fill="#FFFFFF" to fill="currentColor" in all 4 new SVG icons:

  • Bold/outlined.svg
  • Italic/outlined.svg
  • BulletList/outlined.svg
  • NumberedList/outlined.svg

This ensures the icons properly support dark mode and inherit text color from the parent.

8. Bundle size concern: "Did you check how much this tiptap will bloat the build? Maybe it can be lazy loaded if big."

Answer: The author already acknowledged this needs to be addressed. Tiptap + extensions can add significant bundle size (~100-200KB).

Fixed: ✅ Implemented lazy loading using React.lazy() and Suspense:

  • RichTextEditor is now dynamically imported in InlineContentEditor.tsx
  • Shows a Loader component while the editor loads
  • This ensures Tiptap and all its dependencies are only loaded when a user actually opens a job content editor

Summary of Changes

Commit: de483f5

  1. SVG Icons: Fixed all 4 icons to use currentColor instead of hardcoded #FFFFFF for proper dark mode support
  2. Removed redundant cleanup: Cleaned up unnecessary state resets in LinkModal and RichTextToolbar since the useEffect already handles this
  3. Lazy loading: Implemented React.lazy() for RichTextEditor to prevent bundle bloat - Tiptap is now only loaded when needed

All review feedback has been addressed!


- Fix SVG icons to use currentColor for dark mode support
- Remove redundant state cleanup in LinkModal and RichTextToolbar
- Implement lazy loading for RichTextEditor to reduce bundle size

Co-authored-by: Ido Shamun <idoshamun@users.noreply.github.com>
@idoshamun
Copy link
Member Author

@claude we use nextjs dynamic for lazyloading. look for examples

@claude
Copy link
Contributor

claude bot commented Jan 6, 2026

Claude finished @idoshamun's task in 1m 44s —— View job


Refactoring complete! ✅

Tasks

  • Find examples of Next.js dynamic imports in the codebase
  • Read current lazy loading implementation
  • Refactor from React.lazy() to next/dynamic
  • Commit and push changes

Changes made

Commit: dea985f

Refactored the RichTextEditor lazy loading to use next/dynamic instead of React.lazy() following the codebase's established patterns:

  1. Replaced React.lazy() with next/dynamic: Now uses the Next.js dynamic import pattern consistent with the rest of the codebase (e.g., MarkdownInput, modals)
  2. Added proper options:
    • ssr: false - Client-side only rendering for the rich text editor
    • loading: () => <Loader /> - Shows loader while the component loads
  3. Added webpack chunk name: /* webpackChunkName: "richTextEditor" */ for better code splitting and debugging
  4. Removed Suspense wrapper: No longer needed as next/dynamic handles suspense internally
  5. Cleaned up imports: Removed unused lazy and Suspense from React imports

The lazy loading behavior is preserved - Tiptap and its dependencies are only loaded when a user opens a job content editor - but now using the project's preferred Next.js dynamic import pattern.


Commit dea985f

- Replace React.lazy() with next/dynamic following codebase patterns
- Add ssr: false and loading component options
- Add webpack chunk name for better code splitting
- Remove Suspense wrapper (handled by next/dynamic)

Co-authored-by: Ido Shamun <idoshamun@users.noreply.github.com>
Comment on lines 53 to 57
const handleClose = useCallback(() => {
setUrl('');
setLabel('');
onClose();
}, [onClose]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude you can simply just pass onClose to the button no need for this callback function

Co-authored-by: Chris Bongers <rebelchris@users.noreply.github.com>
@idoshamun idoshamun merged commit 8b5a5a4 into main Jan 6, 2026
11 checks passed
@idoshamun idoshamun deleted the feat/wysiwyg-rich-text-editor branch January 6, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants