Conversation
📝 WalkthroughWalkthroughAdds article tags at DB and validation layers; refactors editor into a reusable hook; adds image upload hook and UI, tag input, controlled publish modal with client-side validation and mutation; integrates React Query, global Toaster, new UI primitives; replaces Changes
Sequence DiagramsequenceDiagram
participant User
participant Editor as "Editor UI"
participant ImageUpload as "Image Upload UI"
participant PublishModal as "Publish Modal"
participant API as "Backend API"
participant DB as "Database"
User->>Editor: edit content / open editor
User->>ImageUpload: select image file
ImageUpload->>API: POST /upload (file)
API->>ImageUpload: { image_url }
ImageUpload->>Editor: insert image URL
User->>PublishModal: open modal, add title/tags/preview
PublishModal->>PublishModal: validate with zod (tags, preview)
alt valid
PublishModal->>API: POST /articles (title, content, tags, preview)
API->>DB: insert article (tags: text[])
DB-->>API: success
API-->>PublishModal: created
PublishModal->>User: show success toast / navigate
else invalid
PublishModal->>User: display validation errors
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ui/src/router.tsx (1)
23-27:⚠️ Potential issue | 🟠 MajorRouter type registration loses route-tree specificity.
ReturnType<typeof createRouter>registers the generic router type without the concrete route tree. This meansLink,useParams,useSearch, etc. won't benefit from full type inference of your routes.Suggested fix
declare module '@tanstack/react-router' { interface Register { - router: ReturnType<typeof createRouter> + router: ReturnType<typeof getRouter> } }ui/src/components/write/PublishButton.tsx (1)
176-181:⚠️ Potential issue | 🟠 MajorHardcoded username "Kakaashihatakee".
The "Publishing to" username is hardcoded. This should be sourced from the authenticated user's profile/session data.
🤖 Fix all issues with AI agents
In `@backend/src/db/schema/article.ts`:
- Line 18: The new non-nullable column definition for tags (in article schema,
the line with tags: text("tags").array().notNull()) will fail on migration if
existing rows exist; update the schema to provide a safe default or ensure
backfill: either change the column definition to include a default empty array
(e.g., add .default([]) to the tags column) or add a migration step that
backfills existing article rows with an empty array before applying notNull;
update the Article schema's tags definition (the tags: text(...).array() chain)
and/or the migration script accordingly so existing rows get a value.
In `@ui/eslint.config.js`:
- Around line 7-12: The current eslint flat config combined ignores and rules in
one object so the ignores array ('.output/**') is no longer global; split into
two config objects: keep one object that only has ignores: ['.output/**'] so it
remains a global ignore, and create a separate config object containing the
rules (e.g. '@typescript-eslint/no-unused-vars') and any extends/overrides (e.g.
tanstackConfig) — locate the existing 'ignores' and 'rules' entries in
eslint.config.js and move 'rules' into the second config object while leaving
the first object containing only 'ignores' to preserve global behavior.
In `@ui/src/components/Editor/EditorImageAdd.tsx`:
- Around line 33-36: Replace the silent console.error in the image upload error
handler with a user-facing toast: inside the onError callback in EditorImageAdd
(the onError: (error) => { ... } handler), call toast.error(...) with a short
message (e.g., "Failed to upload image") and include the error message/detail,
and keep or remove the console.error as desired so users see the failure via the
global Sonner Toaster.
In `@ui/src/components/Editor/use-editor.tsx`:
- Around line 29-57: StarterKit is being extended while also registering
standalone Blockquote, Code and CodeBlock variants, causing duplicate extension
registration; remove the separate Blockquote and Code extensions and instead
disable those bundled ones inside StarterKit by calling StarterKit.configure({
blockquote: false, code: false, codeBlock: false }) and keep your specialized
CodeBlockLowlight (or custom Code) extension only; update the extensions array
to use the configured StarterKit and the lowlight CodeBlock extension
(CodeBlockLowlight) without the duplicated Blockquote/Code entries.
- Line 24: The hook useEditor currently declares a non-null return type Editor
but actually returns Editor | null; change the function signature to return
Editor | null (useEditor: UseArticleEditorProps = {}): Editor | null and
propagate that nullability to callers; update consumers such as WriteComponent
where editorInstance.getHTML() is called to guard against null (e.g., check
editorInstance before calling or use optional chaining) so you don't call
getHTML() on a null editorInstance.
In `@ui/src/components/write/ImageUpload.tsx`:
- Around line 33-39: The clickable upload container in ImageUpload.tsx is not
keyboard-accessible; update the div that renders the upload area to include
role="button" and tabIndex={0}, and add a keyboard handler (e.g., onKeyDown)
that calls the existing handleClick when Enter or Space is pressed so keyboard
and assistive tech users can activate the control; also ensure you preserve the
existing onClick and any aria-label/aria-describedby if present for screen
reader context.
- Around line 23-31: The file input change handler handleFileChange currently
calls onFileSelect but never clears the input value, so selecting the same file
twice won't re-trigger onChange; after calling onFileSelect(file) clear the
input's value (e.g., set event.currentTarget.value = '' or use the input ref to
reset value) so subsequent identical selections will emit change events and
re-run onFileSelect.
In `@ui/src/components/write/PublishButton.tsx`:
- Around line 166-170: The paragraph inside the PublishButton component contains
hardcoded placeholder text referencing "Medium's homepage"; update the JSX
within the PublishButton (the <p className="text-xs text-muted-foreground pt-2">
element) to reference your own platform name or a configurable appName
string/prop instead of "Medium" so the copy is accurate and reusable (use an
existing appName constant or add a prop/context lookup if needed).
- Line 119: The toast.error call in PublishButton.tsx currently uses a message
with a trailing colon ("Failed to publish story:"), remove the stray colon so
the message reads "Failed to publish story" (update the toast.error invocation
in the PublishButton component/function where toast.error('Failed to publish
story:') is used); if the code later appends an error string, ensure
spacing/concatenation is correct (e.g., include the error with a separating
colon there instead of in the base message).
- Around line 211-213: In the PublishButton component replace the empty span
shown when isPublishing is true with a visible, sized spinner element (e.g., a
div or inline SVG) that has explicit width/height, border or stroke, and the
animate-spin class so users see publishing activity; ensure the spinner includes
accessible attributes (aria-hidden or aria-label/role as appropriate) and sits
where the current <span className="animate-spin" /> is rendered so styling and
layout remain correct.
- Around line 80-91: handleOpenChange currently resets previewTitle,
previewText, tags and previewImageUrl when opening the dialog but does not clear
validation errors; update the open branch inside the handleOpenChange callback
to also reset the error state (e.g., call the appropriate setter such as
setErrors([]) or resetErrors()) so stale error messages are cleared whenever the
dialog is opened; reference the handleOpenChange function and the state setters
setIsOpen, setPreviewTitle, setPreviewText, setTags, setPreviewImageUrl and the
errors setter (setErrors) when making this change.
- Around line 33-35: Update publishSchema's tag validations: change the element
validator from .min(3, 'Tag cannot be empty') to a message that reflects the
length requirement (e.g., 'Tag must be at least 3 characters') and add an
array-level .max(MAX_TAGS, 'No more than 5 tags allowed') to enforce the tag
count server-side; because MAX_TAGS is currently declared after publishSchema,
either move the const MAX_TAGS = 5 above publishSchema or inline the value 5 in
the .max call. This ensures publishSchema (not just the UI TagInput) enforces
both min length and max tag count.
🧹 Nitpick comments (11)
ui/tsconfig.json (2)
17-22:@/*and@ui/*are redundant — both resolve to./src/*.Having two aliases that map to the same directory invites inconsistent import styles across the codebase. Consider keeping only one (e.g.,
@/) to maintain a single canonical import path.
21-21: Vite resolver is properly configured to pick up@backend/*— the plugin is initialized in vite.config.ts and the backend/src target exists. However, the alias is currently unused in the codebase, and more importantly, there are no build-time or CI checks to detect if this path becomes stale (e.g., if the backend structure changes). Consider adding a validation check in the build or CI pipeline to ensure cross-project path aliases remain valid, or document the dependency to prevent silent breakage during monorepo refactoring.backend/src/db/schema/article.ts (1)
18-18: Consider a GIN index ontagsif you plan to query by tag.Postgres array columns benefit from a GIN index for containment queries (
@>,&&). Without one, tag-based lookups will require a sequential scan.ui/src/components/ui/sonner.tsx (1)
1-4:Reactis referenced (Line 36) but never imported.Line 36 uses
React.CSSPropertiesas a type assertion. While thereact-jsxtransform auto-injects React for JSX, it does not placeReactin the module scope for explicit type references. This may compile depending on global type declarations, but it's fragile and inconsistent.Proposed fix
+import type React from 'react' import { useTheme } from "next-themes" import { Toaster as Sonner, type ToasterProps } from "sonner"ui/src/hooks/use-upload-image.ts (1)
4-17: Clean mutation hook — consider adding client-side file size validation.The hook works correctly. One enhancement to consider: large files will still be sent to the server before being rejected. A client-side size check in the
mutationFn(or upstream in the component) would provide faster feedback and reduce unnecessary network traffic.ui/src/components/Editor/EditorImageAdd.tsx (1)
50-50:urlstate is not reset when the dialog closes.If a user types a URL, then closes the dialog without submitting, the stale URL will persist on next open. Reset it in the
onOpenChangehandler.Proposed fix
- <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> + <Dialog open={isOpen} onOpenChange={(open) => { + if (!open) { + setUrl('') + onClose() + } + }}>backend/src/shared/article.model.ts (1)
27-27: Consider adding per-tag validation and an upper bound on the array.The
z.array(z.string())allows empty strings and arbitrarily long values. There's also no.max()on the array, so a client could submit an unbounded number of tags.Suggested improvement
- tags: z.array(z.string()).min(3, "At least 3 tags are required"), + tags: z.array(z.string().trim().min(1, "Tag cannot be empty").max(50, "Tag is too long")) + .min(3, "At least 3 tags are required") + .max(10, "Too many tags"),ui/src/components/shared/TagInput.tsx (2)
14-14: Space as a trigger prevents multi-word tags.Including
' 'inTAG_INPUT_TRIGGERSmeans users cannot create tags like "machine learning" or "web dev". If multi-word tags are intended, consider removing Space from the triggers and relying on Enter and Comma only.
31-46: Pasted text with delimiters is not split into individual tags.If a user pastes
"react, typescript, node", it will land in the input as a single string and only be added as one tag on the next trigger keypress. Consider adding anonPastehandler that splits on commas and adds multiple tags at once.ui/src/components/Editor/Editor.tsx (1)
19-19: EmptyclassNameattribute.
className=""is a no-op. Consider removing it to keep the JSX clean.♻️ Suggested cleanup
- <EditorContent editor={editor} className="" /> + <EditorContent editor={editor} />ui/src/components/write/PublishButton.tsx (1)
122-122: Use type-safe navigation instead of string interpolation.
router.navigate({ to:/article/${mydata.slug}})bypasses TanStack Router's type-safe route params. Use the params-based API to catch route mismatches at compile time.Proposed fix
- router.navigate({ to: `/article/${mydata.slug}` }) + router.navigate({ to: '/article/$slug', params: { slug: mydata.slug } })
| { | ||
| ignores: ['.output/**'], | ||
| rules: { | ||
| '@typescript-eslint/no-unused-vars': 'warn', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Adding rules here changes ignores from global to local scope.
In ESLint flat config, a config object containing only ignores acts as a global ignore. By adding rules to the same object, ignores now only filters files for this config block — .output/** is no longer globally excluded and will be linted by tanstackConfig rules, likely causing spurious errors on build output.
Split into two config objects to preserve the original global-ignore behavior:
Proposed fix
export default [
...tanstackConfig,
{
ignores: ['.output/**'],
- rules: {
- '@typescript-eslint/no-unused-vars': 'warn',
- },
},
+ {
+ rules: {
+ '@typescript-eslint/no-unused-vars': 'warn',
+ },
+ },
]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| ignores: ['.output/**'], | |
| rules: { | |
| '@typescript-eslint/no-unused-vars': 'warn', | |
| }, | |
| }, | |
| { | |
| ignores: ['.output/**'], | |
| }, | |
| { | |
| rules: { | |
| '@typescript-eslint/no-unused-vars': 'warn', | |
| }, | |
| }, |
🤖 Prompt for AI Agents
In `@ui/eslint.config.js` around lines 7 - 12, The current eslint flat config
combined ignores and rules in one object so the ignores array ('.output/**') is
no longer global; split into two config objects: keep one object that only has
ignores: ['.output/**'] so it remains a global ignore, and create a separate
config object containing the rules (e.g. '@typescript-eslint/no-unused-vars')
and any extends/overrides (e.g. tanstackConfig) — locate the existing 'ignores'
and 'rules' entries in eslint.config.js and move 'rules' into the second config
object while leaving the first object containing only 'ignores' to preserve
global behavior.
| content = '', | ||
| editable = true, | ||
| onUpdate, | ||
| }: UseArticleEditorProps = {}): Editor => { |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
tiptap v3 useEditor hook return type Editor null
💡 Result:
In Tiptap v3 (@tiptap/react), the useEditor(...) hook returns an editor instance or null:
- Return type:
Editor | null(it can benullduring initial render / before initialization, especially with SSR orimmediatelyRender: false). [1]
So in TypeScript you should type/handle it as nullable:
import type { Editor } from '@tiptap/react'
const editor: Editor | null = useEditor({ /* ... */ })
if (!editor) return nullIf you’re using the new composable API, useTiptap() also exposes editor: Editor | null plus an isReady boolean you can guard on. [2]
Sources:
[1] Tiptap React docs (manual setup shows instance/editor as Editor | null) https://tiptap.dev/docs/editor/getting-started/install/react
[2] Tiptap React composable API docs (useTiptap returns { editor: Editor | null, isReady: boolean }) https://tiptap.dev/docs/guides/react-composable-api
🏁 Script executed:
# Search for useEditor usage in the codebase to understand context
fd "Editor.tsx" ui/src/components/Editor/ | head -20Repository: ArifulProtik/MarkLink
Length of output: 100
🏁 Script executed:
cat -n ui/src/components/Editor/Editor.tsx | head -60Repository: ArifulProtik/MarkLink
Length of output: 761
🏁 Script executed:
# Find where useArticleEditor hook is used
rg "useArticleEditor" ui/src --type tsx --type ts -B 2 -A 5Repository: ArifulProtik/MarkLink
Length of output: 92
🏁 Script executed:
# Find where useArticleEditor hook is used
rg "useArticleEditor" ui/src -B 2 -A 5Repository: ArifulProtik/MarkLink
Length of output: 1773
🏁 Script executed:
# Check the full use-editor.tsx implementation
cat -n ui/src/components/Editor/use-editor.tsxRepository: ArifulProtik/MarkLink
Length of output: 2172
🏁 Script executed:
# Check the complete WriteComponent to see how editor is used
cat -n ui/src/components/write/WriteComponent.tsxRepository: ArifulProtik/MarkLink
Length of output: 1184
Return type Editor is incorrect — useEditor returns Editor | null.
useEditor from @tiptap/react returns Editor | null (null during SSR or before initialization). Declaring the return type as Editor silently casts away nullability, which causes type unsafety in consumers. WriteComponent line 13 calls editorInstance.getHTML() without null checks, which will crash at runtime if the editor is null.
Proposed fix
export const useArticleEditor = ({
content = '',
editable = true,
onUpdate,
-}: UseArticleEditorProps = {}): Editor => {
+}: UseArticleEditorProps = {}): Editor | null => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| }: UseArticleEditorProps = {}): Editor => { | |
| }: UseArticleEditorProps = {}): Editor | null => { |
🤖 Prompt for AI Agents
In `@ui/src/components/Editor/use-editor.tsx` at line 24, The hook useEditor
currently declares a non-null return type Editor but actually returns Editor |
null; change the function signature to return Editor | null (useEditor:
UseArticleEditorProps = {}): Editor | null and propagate that nullability to
callers; update consumers such as WriteComponent where editorInstance.getHTML()
is called to guard against null (e.g., check editorInstance before calling or
use optional chaining) so you don't call getHTML() on a null editorInstance.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ui/src/components/write/PublishButton.tsx (1)
178-182:⚠️ Potential issue | 🟡 MinorHardcoded username "Kakaashihatakee" should be dynamic.
This appears to be a placeholder. The publishing target should come from the authenticated user's profile/context, not be hardcoded.
🤖 Fix all issues with AI agents
In `@ui/src/components/Editor/EditorImageAdd.tsx`:
- Around line 26-40: The file input value isn't being cleared in handleUpload so
selecting the same file again won't trigger onChange; after calling uploadImage
(in the success, error, or finally path) reset the input value (e.g., clear
e.currentTarget.value or use a ref to set the input's value = '') so the input
is cleared once the mutation settles; update handleUpload to ensure the input is
reset in the onSuccess/onError or a finally callback around uploadImage while
keeping the existing editor.chain().focus().setImage({ src: uploadedUrl }).run()
and onClose() logic.
- Around line 34-37: The onError handler in EditorImageAdd.tsx (the callback
passed to useUploadImage) uses error.message directly which can be undefined for
the API client's error shape; update the onError to build a safe fallback string
(e.g., use error.message || error?.toString() || JSON.stringify(error) or a
default like "Unknown error") and pass that to toast.error and the console log
so the toast never shows "undefined" and you still log useful info.
🧹 Nitpick comments (2)
ui/src/components/Editor/EditorImageAdd.tsx (1)
42-48: URL input and "Add" button remain enabled while a file upload is in flight.A user could add an image via URL while a file upload is pending, leading to two images inserted or confusing state. Consider disabling the URL flow while
isUploadingis true.Proposed fix
- <Button onClick={handleAddFromUrl}>Add</Button> + <Button onClick={handleAddFromUrl} disabled={isUploading}>Add</Button>ui/src/components/write/PublishButton.tsx (1)
18-18:PublishDatatype alias referencespublishSchemabefore its declaration — works but reads oddly.TypeScript resolves type aliases after full file parsing, so there's no TDZ issue here. However, placing the type after the schema declaration improves readability.
Summary by CodeRabbit
New Features
Style