-
-
Notifications
You must be signed in to change notification settings - Fork 173
Feat/layout updates #1324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/layout updates #1324
Conversation
- Add SidebarContext for managing collapsed state - Create MinimalHeader with hamburger toggle aligned to sidebar icons - Implement AppSidebar component with navigation items - Update sidebar-layout with animated width transitions and clipping - Adjust sidebar padding to p-3 and hover containment with max-w-9 - Add pink accent for current page indicator
- Add aria-label to header elements for accessibility - Update sidebar and navbar layout components - Refactor editor navigation styling - Minor layout adjustments across page components
- Update home test to check for "Trending" heading instead of h1 - Update login test button text to match OAuth buttons - Fix articles test to use Create button and correct modal text - Add loading state wait and exact matching in my-posts test - Remove flaky footer Events link check
- Add getUserLinkBySlug endpoint for fetching user link posts by slug - Fix myScheduled to filter by status="scheduled" instead of "published" - Remove unused gt import from drizzle-orm
- Add PostEditor with WriteTab and LinkTab for article/link creation - Add URL metadata fetching for link posts - Add article toolbar with formatting options - Add E2E editor tests and test utilities - Update E2E setup with link post test data - Update slash command and image upload extensions
- Add user link detail page and fetch-metadata API route - Add NewsletterCTA component - Update draft preview page with improved layout - Update feed, articles, and homepage clients - Improve header, sidebar, and content layout components - Update comments area and unified content card - Minor fixes to Search, listbox, and fallback media components
- Update auth.ts and next-auth types - Add SidebarContext for collapsible sidebar state - Add drizzle seed-sources script - Update comment router and posts lib - Update markdoc editor shortcuts and example tags - Remove unused CoduChallenge component
|
@NiallJoeMaher is attempting to deploy a commit to the Codú Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughIntroduces a comprehensive link-sharing feature with a new two-tab editor system (Write/Link), metadata fetching API, link post rendering components, and associated sidebar/header layout refactoring. Includes newsletter CTA integration, theme toggle settings, database seeding for RSS sources, and extensive end-to-end test coverage. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant LinkTab
participant API as /api/fetch-metadata
participant Preview as UrlMetadataPreview
User->>LinkTab: Enter URL
LinkTab->>LinkTab: Debounce URL change (500ms)
LinkTab->>API: POST with URL
API->>API: Validate & fetch HTML
API->>API: Extract metadata (title, image, readTime)
API-->>LinkTab: Return metadata
LinkTab->>Preview: Render preview card
Preview-->>User: Display image, title, description, read time
sequenceDiagram
participant Editor as PostEditor
participant WriteTab
participant LinkTab
participant Draft as Auto-Save
participant API as Publish API
Editor->>Editor: Initialize with active tab
alt Write Tab
Editor->>WriteTab: Render editor + toolbar
WriteTab->>WriteTab: Edit title/body/tags
WriteTab->>Draft: Trigger save (debounced)
else Link Tab
Editor->>LinkTab: Render URL input + preview
LinkTab->>LinkTab: Fetch metadata on URL change
Editor->>Editor: Validate link inputs
end
User->>Editor: Click Publish
Editor->>API: Send post data (title, body/url, tags, type)
API-->>Editor: Return post ID
Editor->>Editor: Show confirmation modal
User->>Editor: Confirm "Let's do this!"
Editor->>API: Publish post
API-->>Editor: Success
Editor-->>User: Redirect to published post
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
app/(app)/auth/page.tsx (1)
30-65: Restore semantic HTML: use<main>instead of<div>.Replacing the
<main>element with a<div>removes essential landmark navigation for screen reader users. The<main>element identifies the primary content area and allows assistive technology users to skip directly to the main content.This change conflicts with the accessibility improvements made elsewhere in this PR (adding
aria-labelattributes to navigation elements).♿ Recommended fix to restore semantic HTML
- <div className="flex w-full flex-grow flex-col justify-center bg-neutral-100 px-4 py-20 dark:bg-black sm:px-6 lg:py-40"> + <main className="flex w-full flex-grow flex-col justify-center bg-neutral-100 px-4 py-20 dark:bg-black sm:px-6 lg:py-40"> <div className="flex flex-shrink-0 justify-center"> <Link href="/"> <span className="sr-only">Codú</span> @@ -62,7 +62,7 @@ </div> </div> - </div> + </main>app/(app)/error.tsx (1)
8-54: Restore semantic HTML: use<main>instead of<div>.The same accessibility concern applies here: replacing
<main>with<div>removes the primary content landmark. Error pages require proper semantic structure just like other pages to ensure screen reader users can navigate effectively.♿ Recommended fix to restore semantic HTML
- <div className="flex w-full flex-grow flex-col justify-center bg-white px-4 py-20 sm:px-6 lg:py-40"> + <main className="flex w-full flex-grow flex-col justify-center bg-white px-4 py-20 sm:px-6 lg:py-40"> <div className="flex flex-shrink-0 justify-center"> <Link href="/"> <span className="sr-only">Codú</span> @@ -51,6 +51,6 @@ </div> </div> - </div> + </main>components/editor/editor/index.tsx (1)
35-40: Address accessibility issues before removing ESLint suppression.Removing the ESLint disable comment without fixing the underlying issues will cause linting failures and leaves keyboard accessibility problems unresolved. The
divelement withonClick(lines 36-40) lacks keyboard event handlers, making it inaccessible to keyboard-only users.Options:
- Recommended: Add keyboard accessibility (make the container keyboard-focusable):
♿ Proposed fix to add keyboard support
- <div - className="relative" - onClick={() => { - editor?.chain().focus().run(); - }} - > + <div + className="relative" + role="button" + tabIndex={0} + onClick={() => { + editor?.chain().focus().run(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + editor?.chain().focus().run(); + } + }} + >
- Alternative: Restore the ESLint suppression until the accessibility issues are properly addressed:
Temporarily restore suppression
// TODO: Review this for no-static-element-interactions click-events-have-key-events - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} <divapp/(editor)/create/[[...paramsArr]]/_client.tsx (1)
196-200: Pipeline failure:setStatewithin effect causes cascading renders.The linter flags
setShouldRefetchat line 198. This pattern can cause unnecessary re-renders.Suggested fix
useEffect(() => { - if (shouldRefetch) { - setShouldRefetch(!(dataStatus === "success")); - } + if (shouldRefetch && dataStatus === "success") { + setShouldRefetch(false); + } }, [dataStatus, shouldRefetch]);Alternatively, consider removing
shouldRefetchstate entirely and relying on the query'senabledoption with a ref to track if initial fetch occurred.
🤖 Fix all issues with AI agents
In @app/(app)/[username]/[slug]/_userLinkDetail.tsx:
- Around line 192-193: The variable faviconUrl is computed via
getFaviconUrl(externalUrl) but never used; either remove that assignment or
render the favicon next to the hostname in the UI: use faviconUrl (when
externalUrl is truthy) and render an <img> or Avatar component beside where
hostname (from getHostname(externalUrl)) is displayed (refer to the hostname
variable and the render around the hostname at the user link detail JSX),
ensuring you handle missing faviconUrl and set alt/title and small size for
layout.
- Around line 95-99: Move the conditional state updates out of render into a
useEffect: import useEffect, then create an effect that watches [serverVoteKey,
lastSyncedKey, linkContent, currentUserVote, currentUpvotes, currentDownvotes]
and inside the effect check if (serverVoteKey !== lastSyncedKey && linkContent)
and then call setUserVote(currentUserVote), setVotes({ upvotes: currentUpvotes,
downvotes: currentDownvotes }), and setLastSyncedKey(serverVoteKey); this
preserves the existing logic but avoids setting state during render and prevents
unnecessary re-renders.
In @app/(app)/advertise/_client.tsx:
- Around line 13-19: The fragment wrapper for the advertise page removes the
semantic <main> landmark; restore accessibility by wrapping the page sections
(HeroSection, MetricsSection, OfferingsSection, SocialProofSection,
ContactSection) in a <main> element unless a parent layout (e.g.,
SidebarAppLayout) already provides a single consistent <main> for all pages—if
the layout supplies it, ensure this page remains nested inside that main;
otherwise replace the fragment with a <main> to reintroduce the primary content
landmark.
In @app/(app)/settings/_client.tsx:
- Around line 438-462: resolvedTheme from useTheme() can be undefined during
SSR/initial render causing hydration mismatches; update the Switch checked prop
to derive its value safely (e.g., use resolvedTheme ?? theme ?? "light") so it
defaults to a known value and use that same derived value for rendering logic,
e.g. replace checked={resolvedTheme === "dark"} with checked={(resolvedTheme ??
theme ?? "light") === "dark"} and keep onChange={(checked) => setTheme(checked ?
"dark" : "light")} so setTheme still controls updates.
In @app/(editor)/create/[[...paramsArr]]/_client.tsx:
- Around line 416-422: The effect mutates postStatus via setPostStatus based on
(title + body).length which triggers cascading renders; instead derive
postStatus from title/body and remove state updates in the effect: stop calling
setPostStatus inside the useEffect that references title, body and postStatus,
replace the mutable postStatus state with a computed value (e.g., compute
currentStatus = (title + body).length < 5 ? null : status.DRAFT) and use
currentStatus wherever postStatus was used, or keep a single setter only for
explicit user actions; update references to title, body, postStatus,
setPostStatus and status.DRAFT accordingly and remove the effect entirely.
- Around line 377-413: The effect watches both data and activeTab but calls
setActiveTab inside it, risking cascading renders; change the dependency array
to only [data] so the effect runs when data changes (keep the existing guard if
(activeTab !== "link") before calling setActiveTab("link")), i.e., update the
useEffect dependency from [data, activeTab] to [data] while retaining the
current conditional logic around setActiveTab, referencing the useEffect,
activeTab, setActiveTab, data, and postType symbols.
In @app/global-error.tsx:
- Line 21: Remove the dead JSX expression `{}` from the GlobalError component
(the empty JSX at the top of app/global-error.tsx) — it serves no purpose;
delete it and, if the previous eslint-disable was intended to silence a type
issue, instead fix the underlying type-safety problem referenced by the next
expression in the component (address the type or prop checks used on the
following line) rather than reintroducing a no-op placeholder.
In @components/editor/editor/extensions/slash-command.tsx:
- Around line 238-241: The useLayoutEffect in the SlashCommand/CommandList
component currently calls setSelectedIndex(0) synchronously, causing a
react-hooks/set-state-in-effect violation; remove that direct state mutation
from useLayoutEffect and instead reset selection by giving the CommandList (or
the component that owns selectedIndex) a changing key derived from items (e.g.,
itemsKey) so React remounts and resets state, or, if remounting is not
acceptable, replace the state reset with a mutable ref (selectedIndexRef) and
compute selectedIndex as derived from items without calling setState inside the
layout effect; update usages of selectedIndex/selectedIndexRef and the parent
that renders CommandList/SlashCommand to pass the key when choosing the remount
approach.
In @components/markdocNodes/FallbackMedia/FallbackMedia.tsx:
- Around line 8-10: The iframe in FallbackMedia is missing a required title and
contains an extraneous empty JSX expression; remove the `{}` and accept/require
a title prop on the FallbackMedia component (or read props.title) and pass it
into the iframe, using a sensible accessible fallback like "Embedded content"
when no title is provided so screen readers can identify the iframe content;
update the component that renders the iframe (FallbackMedia / the JSX using
<iframe {...props} />) to supply title={props.title ?? 'Embedded content'} and
ensure callers can override it.
In @components/PostEditor/components/TagInput.tsx:
- Around line 45-59: The addTag callback does not enforce the database length
limit for Tag.title (varchar(20)), so validate the trimmedTag length before
adding: check trimmedTag.length <= 20 and if it exceeds, do not call onChange;
instead surface feedback (e.g., set an error state or update helpText) so the UI
informs the user of the 20-character limit; update the dependency list if you
add new state setters and keep existing checks (tags.includes, maxTags) and
calls to onChange and setInputValue intact.
In @components/PostEditor/PostEditor.tsx:
- Around line 118-132: The handlers handleTitleChange and handleBodyChange call
setTitle/setBody then immediately call onContentChange(getCurrentData()), which
reads stale state because React updates are async; remove the immediate
onContentChange call from these handlers and instead trigger onContentChange
from a useEffect that depends on the real state values (title, body, tags, etc.)
so getCurrentData() runs after state is updated, or alternatively have the
handlers compute the new data object from the incoming newTitle/newBody and call
onContentChange with that computed object (e.g., build the same shape
getCurrentData returns) to avoid stale reads.
In @components/PostEditor/tabs/WriteTab.tsx:
- Around line 53-63: handleMarkdownImageUpload currently calls uploadImage()
which inserts images only into the TipTap editor (via
triggerImageUpload/editor.view) and never updates the markdown textarea state;
change handleMarkdownImageUpload to detect markdown mode (use your existing mode
flag such as isMarkdownMode or the editor mode getter) and when in markdown mode
perform the upload and then append the markdown image syntax (``)
into markdownContent using the setMarkdownContent state updater after the upload
completes; if uploadImage/triggerImageUpload does not return the uploaded URL,
modify uploadImage (or add a callback) to return the image URL so
handleMarkdownImageUpload can construct and insert the markdown string, and
still clear markdownFileInputRef.current.value = "" afterward.
In @CONTRIBUTING.md:
- Line 123: The phrase "To create a e2e test make a file in `/e2e` directory"
uses the wrong article; update the sentence in CONTRIBUTING.md so the phrase
reads "To create an e2e test make a file in `/e2e` directory" (replace "a e2e"
with "an e2e") to correct the grammar.
In @EDITOR_SHORTCUTS.MD:
- Line 24: Update the sentence that reads "Press the meta key (windows key or
mac cmd key) with the desired hotkey eg, cmd+1 to render #" to capitalize the
proper noun "Windows" (i.e., change "windows key" to "Windows key") and ensure
"mac" is styled consistently (e.g., "Mac" or "macOS") if desired for consistency
with project style.
- Line 25: Update the sentence in EDITOR_SHORTCUTS.MD to hyphenate the compound
adjective by changing "double click the word" to "double-click the word" so the
phrase correctly reads "...and for this you can double-click the word or phrase
or press meta+backspace..." ensuring compound adjective style is consistent.
- Line 40: Update the table row to capitalize the proper noun "YouTube" in both
places: change the command label `/youtube` to `/YouTube` and the shortcode
example `{% youtube src="url" /%}` to `{% YouTube src="url" /%}` so both
occurrences use the correct capitalization.
🧹 Nitpick comments (27)
components/ui-components/link.tsx (1)
12-18: Consider adding accessibility indicator for external links.External links that open in new tabs should include an accessibility indicator for screen reader users. The current implementation correctly applies
rel="noopener noreferrer"for security, but lacks an indication that the link opens in a new window.♿ Proposed accessibility enhancement
if (isExternal) { return ( <Headless.DataInteractive> - <a {...props} ref={ref} target="_blank" rel="noopener noreferrer" /> + <a + {...props} + ref={ref} + target="_blank" + rel="noopener noreferrer" + aria-label={props['aria-label'] || `${props.children} (opens in new tab)`} + /> </Headless.DataInteractive> ); }Alternatively, you can add a visual indicator (icon) or let consumers provide their own aria-label.
server/lib/posts.ts (2)
84-110: Consider null handling for user data.The
leftJoinwith the user table (line 104) allows for null user fields if the user record is missing. While the schema enforces a foreign key with cascade delete (making this rare), defensive programming suggests either:
- Using an
innerJoininstead, which would return null for the entire post if user is missing- Adding explicit null checks and handling for user fields before constructing the user object (lines 137-141)
This prevents potential issues if database inconsistencies occur or consumers expect non-null user data.
♻️ Option 1: Use innerJoin for stricter guarantee
.from(posts) - .leftJoin(user, eq(posts.authorId, user.id)) + .innerJoin(user, eq(posts.authorId, user.id)) .where(eq(posts.id, id)) .limit(1);
114-122: Consider performance implications of two separate queries.Splitting tag retrieval into a separate query (lines 114-122) results in two database round trips instead of one. While this approach offers clarity and flexibility, it may have performance implications if this function is called frequently.
Consider:
- Keeping the two-query approach if it's simpler to maintain
- Using a single query with proper joins if performance becomes a concern
- Adding caching if this endpoint is heavily trafficked
components/Comments/CommentsArea.tsx (1)
187-188: Replaceas anytype assertion with proper typing.The
as anycast bypasses type safety. SinceCommentsandChildrentypes are already defined in this file (lines 119-120), consider defining a proper recursive type or using a type guard to safely access thechildrenproperty.♻️ Proposed type-safe approach
Define a proper recursive comment type at the top of the file with the other types:
+type CommentWithChildren = NonNullable<Comments>[number] & { + children?: CommentWithChildren[]; +}; + type Comments = typeof comments; type Children = typeof firstChild; type FieldName = "comment" | "reply" | "edit";Then update the usage:
- - const children = (comment as any).children; + const children = (comment as CommentWithChildren).children;components/editor/editor/plugins/upload-images.tsx (1)
21-22: Restore ESLint suppression or fix the underlying type issue.The ESLint disable comment was removed, but the underlying type assertion
as anyon line 22 remains. This bypasses type safety without addressing the root cause. The TODO comment on line 7 indicates there are known type issues in this file.Consider one of the following approaches:
- Restore the ESLint suppression with a comment explaining why the type assertion is necessary
- Fix the type issue properly by defining the correct type for the transaction metadata
Option 1: Restore suppression with explanation
set = set.map(tr.mapping, tr.doc); // See if the transaction adds or removes any placeholders +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ProseMirror meta requires any for plugin-specific data const action = tr.getMeta(uploadKey as any);Option 2: Define proper type for metadata
+type UploadAction = + | { add: { id: object; pos: number; src: string | ArrayBuffer } } + | { remove: { id: object } } + | undefined; + const UploadImagesPlugin = () => new Plugin({ key: uploadKey as unknown as PluginKey, state: { init() { return DecorationSet.empty; }, apply(tr, set) { set = set.map(tr.mapping, tr.doc); // See if the transaction adds or removes any placeholders - const action = tr.getMeta(uploadKey as any); + const action = tr.getMeta(uploadKey as any) as UploadAction;components/NewsletterCTA/NewsletterCTA.tsx (1)
37-54: Consider adding aria-label to the disabled button for better accessibility.The disabled "Subscribed" button would benefit from an explicit
aria-labelto provide context to screen reader users.♿ Suggested accessibility enhancement
{isSubscribed ? ( <button disabled + aria-label="Already subscribed to newsletter" className="flex w-full cursor-not-allowed items-center justify-center gap-2 rounded-lg bg-white/80 px-6 py-2.5 text-sm font-semibold text-pink-600" >context/SidebarContext.tsx (1)
26-28: Optimize toggleSidebar to avoid unnecessary re-creation.The
toggleSidebarcallback includesisCollapsedin its dependency array, which causes the function to be recreated on every state change. Since you're using the updater form ofsetIsCollapsed, you can reference the current state without including it in dependencies.♻️ Proposed optimization
- const toggleSidebar = useCallback(() => { - setIsCollapsed(!isCollapsed); - }, [isCollapsed, setIsCollapsed]); + const toggleSidebar = useCallback(() => { + setIsCollapsed((prev) => !prev); + }, [setIsCollapsed]);components/PostEditor/components/UrlMetadataPreview.tsx (1)
84-84: Remove empty comment.The empty comment
{}on line 84 serves no purpose and should be removed.♻️ Cleanup suggestion
- {} <imgapp/api/fetch-metadata/route.ts (1)
147-157: DuplicateisValidUrlfunction exists inutils/utils.ts.This function duplicates
isValidUrlfromutils/utils.ts(shown in relevant code snippets). The only difference is that this version explicitly checks forhttp:orhttps:protocols, which is a stricter and more secure validation for this use case.Consider either:
- Importing and extending the existing utility, or
- Extracting this stricter version to a shared location
app/(app)/[username]/[slug]/page.tsx (1)
105-179: Consider consolidatinggetUserPostandgetUserLinkPosthelpers.These two functions share ~90% identical code. The only differences are:
- Line 70 vs 146:
eq(posts.type, "article")vseq(posts.type, "link")- A few additional fields in
getUserLinkPost(externalUrl, coverImage)Consider extracting a shared helper that accepts
typeand optional field selections as parameters.♻️ Suggested consolidation
async function getUserPostByType( username: string, postSlug: string, postType: "article" | "link" ) { const userRecord = await db.query.user.findFirst({ columns: { id: true }, where: eq(user.username, username), }); if (!userRecord) return null; const baseFields = { id: posts.id, title: posts.title, body: posts.body, excerpt: posts.excerpt, slug: posts.slug, status: posts.status, publishedAt: posts.publishedAt, updatedAt: posts.updatedAt, readingTime: posts.readingTime, showComments: posts.showComments, upvotesCount: posts.upvotesCount, downvotesCount: posts.downvotesCount, type: posts.type, // Author fields... authorId: user.id, authorName: user.name, authorImage: user.image, authorUsername: user.username, authorBio: user.bio, // Link-specific (null for articles) externalUrl: posts.externalUrl, coverImage: posts.coverImage, canonicalUrl: posts.canonicalUrl, }; const postResults = await db .select(baseFields) .from(posts) .leftJoin(user, eq(posts.authorId, user.id)) .where( and( eq(posts.slug, postSlug), eq(posts.authorId, userRecord.id), eq(posts.status, "published"), eq(posts.type, postType), lte(posts.publishedAt, new Date().toISOString()), ), ) .limit(1); // ... rest of mapping logic }app/(app)/[username]/[slug]/_userLinkDetail.tsx (1)
101-123: Optimistic updates look good, but consider query invalidation.The optimistic update pattern is correctly implemented. However, the mutation doesn't invalidate or update the query cache on success, which could lead to stale data if the user navigates away and returns.
Consider adding
onSuccessto invalidate the query:const utils = api.useUtils(); const { mutate: vote, status: voteStatus } = api.content.vote.useMutation({ onMutate: async ({ voteType }) => { // ... existing optimistic update }, onError: () => { // ... existing error handling }, onSuccess: () => { utils.content.getUserLinkBySlug.invalidate({ username, slug: contentSlug }); }, });components/PostEditor/tabs/LinkTab.tsx (2)
37-52: Redundant title auto-population logic.The title is auto-populated in two places: the
onSuccesscallback (lines 40-41) and theuseEffect(lines 48-50). While both check!titlebefore updating, this duplication could lead to confusion.Consider consolidating this logic in one place—either keep it solely in
onSuccessor solely inuseEffect.Suggested simplification (remove useEffect)
- // Update title when metadata changes (if title is still empty) - useEffect(() => { - if (metadata?.title && !title) { - onTitleChange(metadata.title); - } - }, [metadata?.title, title, onTitleChange]);The
onSuccesscallback already handles this case when metadata is first fetched.
83-97: Magic number in URL length check.The condition
url.length > 10is arbitrary. Short URLs likehttp://t.co/abc(15 chars) would pass, but the threshold seems disconnected from actual URL validity.Consider using the
isValidUrlhelper (already used byuseLinkMetadata) or simply relying on the existingisLoading || error || metadataconditions.Suggested change
- {(url.length > 10 || isLoading || error || metadata) && ( + {(isLoading || error || metadata) && (The hook already handles URL validation internally, so the preview section will appear once a fetch starts.
server/api/router/content.ts (1)
1179-1180: Consider adding length constraint to username input.The
usernamefield usesz.string()without length constraints. The user table enforces a 40-character limit. Consider adding.max(40)for consistency:Suggested change
- .input(GetContentBySlugSchema.extend({ username: z.string() })) + .input(GetContentBySlugSchema.extend({ username: z.string().min(1).max(40) }))drizzle/seed-sources.ts (2)
18-25: Slug generation may preserve leading/trailing hyphens.If a source name has leading/trailing spaces, they become hyphens before
trim()can act (sincetrim()only removes whitespace). For the current hardcoded data this isn't an issue, but consider:More robust slug generation
const generateSlug = (name: string): string => { return name .toLowerCase() + .trim() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-") - .trim(); + .replace(/^-|-$/g, ""); };
424-500: Seed script processes sources sequentially without transaction.The script creates users and sources one at a time. If it fails mid-execution, partial data remains in the database. For a seed script this is generally acceptable (re-run is idempotent due to existence checks), but consider wrapping in a transaction if atomicity is needed.
components/PostEditor/hooks/useLinkMetadata.ts (2)
38-47: Consider reusing the existingisValidUrlutility.There's already an
isValidUrlfunction inutils/utils.ts. While this version adds protocol validation (http/https only), consider extending the shared utility instead of duplicating it. If the protocol check is specific to this use case, at minimum rename this toisValidHttpUrlfor clarity.-function isValidUrl(url: string): boolean { +function isValidHttpUrl(url: string): boolean {
66-128: Potential stale closure withmetadatain dependency array.Including
metadatain theuseCallbackdependency array meansfetchMetadatais recreated whenevermetadatachanges. This could cause the effect at line 158 to re-run unnecessarily sincefetchMetadatais in its deps. The de-duplication check at line 69 relies onlastFetchedUrlRefanyway.Suggested fix
const fetchMetadata = useCallback( async (targetUrl: string) => { // Skip if already fetched this URL - if (targetUrl === lastFetchedUrlRef.current && metadata) { + if (targetUrl === lastFetchedUrlRef.current) { return; } // ... rest of function }, - [metadata, onSuccess, onError], + [onSuccess, onError], );components/Theme/ThemeProvider.tsx (1)
6-12: Consider respecting system preference.Setting
defaultTheme="dark"forces dark mode for new users. Consider usingenableSystemwithdefaultTheme="system"to respect OS-level dark mode preferences for a better default UX.- <ThemeProvider attribute="class" defaultTheme="dark"> + <ThemeProvider attribute="class" defaultTheme="system" enableSystem>If dark mode is an intentional design choice, this is fine as-is.
e2e/my-posts.spec.ts (1)
14-19: Consider replacing the fixed timeout with a proper wait condition.The loading indicator wait is good for test stability, but the additional 500ms
waitForTimeoutis a code smell that may mask a race condition. Fixed timeouts make tests either flaky (if too short) or unnecessarily slow (if too long).♻️ Consider waiting for actual content instead
// Wait for content to load - wait for loading message to disappear await expect(page.getByText("Fetching your posts...")).toBeHidden({ timeout: 15000, }); - // Additional wait for content to render - await page.waitForTimeout(500); + // Wait for actual content to be present + await page.waitForSelector("article, [data-testid='empty-state']", { + timeout: 5000, + });This waits for either an article or empty state to appear, which is a more reliable condition than an arbitrary timeout.
components/Header/MinimalHeader.tsx (2)
39-41: Consider simplifying the boolean expression.The
enabledoption can be simplified.✨ Suggested simplification
const { data: count } = api.notification.getCount.useQuery(undefined, { - enabled: session ? true : false, + enabled: !!session, });
144-148: Consider usingnext/imagefor the user avatar.Using the native
<img>tag bypasses Next.js image optimization. SinceImagefromnext/imageis already imported (line 18), consider using it for consistency and performance benefits.✨ Suggested change
{session.user?.image ? ( - <img + <Image className="h-7 w-7 rounded-full bg-neutral-300 object-cover lg:h-8 lg:w-8" src={session.user.image} alt={`${session.user?.name}'s avatar`} + width={32} + height={32} + unoptimized />Note:
unoptimizedmay be needed if user images come from external domains not configured innext.config.js.e2e/utils/utils.ts (1)
150-190: **Handle potential undefined return when usingonConflictDoNothing().**When usingonConflictDoNothing()with.returning(), the result is an empty array[]if there's a conflict. This meansresult[0]will beundefinedwhen a conflict occurs, and the function will silently returnundefinedwithout indicating whether the insert succeeded or was skipped due to conflict.For E2E test utilities, this may be acceptable since tests typically use unique slugs, but consider adding a check for clarity.
✨ Optional: Add explicit undefined check
.onConflictDoNothing() .returning(); - return result[0]; + if (!result[0]) { + throw Error(`E2E test link post not created (likely slug conflict): ${slug}`); + } + return result[0];components/SideBar/AppSidebar.tsx (1)
180-180: Remove unnecessary.toLowerCase()oncustomStyle.The
customStylevalues in thesocialLinksarray are already lowercase (e.g.,"hover:bg-twitter focus:bg-twitter"). The.toLowerCase()call is redundant.✨ Suggested fix
- className={`focus-style rounded-md p-1 text-neutral-400 transition-all duration-300 hover:scale-105 hover:text-white hover:brightness-110 focus:scale-105 focus:text-white focus:brightness-110 ${item.customStyle.toLowerCase()}`} + className={`focus-style rounded-md p-1 text-neutral-400 transition-all duration-300 hover:scale-105 hover:text-white hover:brightness-110 focus:scale-105 focus:text-white focus:brightness-110 ${item.customStyle}`}e2e/editor.spec.ts (1)
446-460: Duplicate tag test assertion may be fragile.Using
span:has-text("duplicate")to count tag instances could match unintended elements. Consider using a more specific selector tied to the tag badge component.✨ Suggested improvement
If tags have a specific class or data attribute, use that instead:
// Example: if tag badges have a specific class const tagCount = await page.locator('.tag-badge:has-text("duplicate")').count(); // Or use a data attribute const tagCount = await page.locator('[data-testid="tag"]:has-text("duplicate")').count();components/ui-components/sidebar-layout.tsx (2)
24-30: Missingfillattribute for color inheritance.Unlike
MenuIcon, this SVG is missingfill="currentColor", which may cause the icon to not inherit the text color properly in dark mode or themed contexts.Suggested fix
function CloseMenuIcon() { return ( - <svg data-slot="icon" viewBox="0 0 20 20" aria-hidden="true"> + <svg data-slot="icon" viewBox="0 0 20 20" aria-hidden="true" fill="currentColor"> <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" /> </svg> ); }
122-126: Consider using a more specific transition property.Using
transition-allon the main content area could trigger transitions on unintended properties. Since only the left padding needs to animate, a more specific transition would be slightly more performant.Suggested refinement
<main - className={`flex flex-1 flex-col transition-all duration-300 ease-in-out lg:min-w-0 lg:pt-16 ${contentPadding}`} + className={`flex flex-1 flex-col transition-[padding] duration-300 ease-in-out lg:min-w-0 lg:pt-16 ${contentPadding}`} >
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (7)
.github/workflows/e2e-tests.ymlis excluded by!**/*.yml.github/workflows/greetings.ymlis excluded by!**/*.yml.github/workflows/pull-request.ymlis excluded by!**/*.ymlicons/linkedin.svgis excluded by!**/*.svg,!**/*.svgpackage.jsonis excluded by!**/*.jsonpublic/images/codu-logo.pngis excluded by!**/*.png,!**/*.pngseed.ymlis excluded by!**/*.yml
📒 Files selected for processing (80)
CONTRIBUTING.mdEDITOR_SHORTCUTS.MDLICENSE.mdapp/(app)/[username]/[slug]/_userLinkDetail.tsxapp/(app)/[username]/[slug]/page.tsxapp/(app)/[username]/_sourceProfileClient.tsxapp/(app)/[username]/_usernameClient.tsxapp/(app)/advertise/_client.tsxapp/(app)/articles/_client.tsxapp/(app)/auth/page.tsxapp/(app)/code-of-conduct/page.tsxapp/(app)/draft/[id]/page.tsxapp/(app)/error.tsxapp/(app)/feed/[sourceSlug]/_client.tsxapp/(app)/feed/_client.tsxapp/(app)/forbidden/page.tsxapp/(app)/jobs/create/_client.tsxapp/(app)/layout.tsxapp/(app)/page.tsxapp/(app)/settings/_client.tsxapp/(editor)/create/[[...paramsArr]]/_client.tsxapp/(editor)/create/[[...paramsArr]]/navigation.tsxapp/api/fetch-metadata/route.tsapp/global-error.tsxauth.tscdk/README.mdcomponents/CoduChallenge/CoduChallenge.tsxcomponents/Comments/CommentsArea.tsxcomponents/ContentDetail/Layout.tsxcomponents/Header/MinimalHeader.tsxcomponents/Layout/SidebarAppLayout.tsxcomponents/Nav/Nav.tsxcomponents/NewsletterCTA/NewsletterCTA.tsxcomponents/PostEditor/PostEditor.tsxcomponents/PostEditor/components/TagInput.tsxcomponents/PostEditor/components/UrlMetadataPreview.tsxcomponents/PostEditor/extensions/index.tscomponents/PostEditor/extensions/s3-image-upload.tscomponents/PostEditor/hooks/useArticleEditor.tscomponents/PostEditor/hooks/useLinkMetadata.tscomponents/PostEditor/index.tscomponents/PostEditor/tabs/LinkTab.tsxcomponents/PostEditor/tabs/WriteTab.tsxcomponents/PostEditor/toolbar/ArticleToolbar.tsxcomponents/SideBar/AppSidebar.tsxcomponents/Theme/ThemeProvider.tsxcomponents/TrendingPosts/TrendingPostsLoading.tsxcomponents/UnifiedContentCard/UnifiedContentCard.tsxcomponents/editor/editor/extensions/slash-command.tsxcomponents/editor/editor/index.tsxcomponents/editor/editor/plugins/upload-images.tsxcomponents/markdocNodes/FallbackMedia/FallbackMedia.tsxcomponents/ui-components/link.tsxcomponents/ui-components/listbox.tsxcomponents/ui-components/navbar.tsxcomponents/ui-components/sidebar-layout.tsxcomponents/ui-components/sidebar.tsxcomponents/ui-components/stacked-layout.tsxcomponents/ui/Search.tsxconfig/site_settings.tscontext/SidebarContext.tsxcurriculum/intro-to-react/introduction.mddrizzle/0016_migrate_comments.sqldrizzle/seed-sources.tse2e/articles.spec.tse2e/constants/constants.tse2e/editor.spec.tse2e/home.spec.tse2e/login.spec.tse2e/my-posts.spec.tse2e/saved.spec.tse2e/setup.tse2e/utils/utils.tsmarkdoc/editor/shortcuts/shortcuts.markdoc.tsmarkdoc/tags/markdoc-example.markdoc.tsnext-env.d.tsserver/api/router/comment.tsserver/api/router/content.tsserver/lib/posts.tstypes/next-auth.d.ts
💤 Files with no reviewable changes (3)
- components/ui-components/listbox.tsx
- components/CoduChallenge/CoduChallenge.tsx
- components/Nav/Nav.tsx
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2024-10-18T04:18:28.906Z
Learnt from: RangerCreaky
Repo: codu-code/codu PR: 1146
File: server/api/router/series.ts:109-135
Timestamp: 2024-10-18T04:18:28.906Z
Learning: In the TypeScript file `server/api/router/series.ts`, when comments are included to aid reviewers, avoid suggesting their removal.
Applied to files:
components/editor/editor/extensions/slash-command.tsxnext-env.d.tscomponents/Comments/CommentsArea.tsxserver/api/router/comment.ts
📚 Learning: 2024-10-22T08:43:13.236Z
Learnt from: dineshsutihar
Repo: codu-code/codu PR: 1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.236Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.
Applied to files:
app/(app)/forbidden/page.tsxapp/(app)/[username]/[slug]/_userLinkDetail.tsxapp/(app)/draft/[id]/page.tsxapp/(editor)/create/[[...paramsArr]]/_client.tsxapp/(app)/feed/[sourceSlug]/_client.tsxapp/(app)/[username]/[slug]/page.tsxapp/(app)/page.tsx
📚 Learning: 2024-10-28T23:51:53.631Z
Learnt from: JohnAllenTech
Repo: codu-code/codu PR: 1187
File: e2e/setup.ts:23-25
Timestamp: 2024-10-28T23:51:53.631Z
Learning: In the e2e tests setup scripts, it's acceptable to hardcode IDs for test data.
Applied to files:
e2e/constants/constants.ts
📚 Learning: 2024-10-20T01:10:48.663Z
Learnt from: JohnAllenTech
Repo: codu-code/codu PR: 1158
File: e2e/articles.spec.ts:208-224
Timestamp: 2024-10-20T01:10:48.663Z
Learning: In `e2e/articles.spec.ts`, tests within the `Authenticated Articles Page` describe block assume the user is already authenticated, so explicit authentication steps are not necessary within individual tests.
Applied to files:
e2e/articles.spec.tse2e/editor.spec.tse2e/saved.spec.tse2e/home.spec.tse2e/login.spec.ts
📚 Learning: 2024-10-17T01:03:59.199Z
Learnt from: JohnAllenTech
Repo: codu-code/codu PR: 1139
File: e2e/articles.spec.ts:133-147
Timestamp: 2024-10-17T01:03:59.199Z
Learning: In `e2e/articles.spec.ts` (TypeScript Playwright tests), avoid refactoring duplicated code into helper functions unless the code is reused elsewhere.
Applied to files:
e2e/articles.spec.tse2e/editor.spec.tse2e/my-posts.spec.tse2e/saved.spec.tse2e/home.spec.tse2e/login.spec.ts
📚 Learning: 2024-10-17T01:07:41.406Z
Learnt from: JohnAllenTech
Repo: codu-code/codu PR: 1139
File: e2e/articles.spec.ts:0-0
Timestamp: 2024-10-17T01:07:41.406Z
Learning: In our end-to-end tests, using `waitUntil: "networkidle"` in `page.waitForNavigation` is unreliable because of ongoing background requests, so it should be avoided.
Applied to files:
e2e/articles.spec.ts
📚 Learning: 2024-11-09T15:07:15.871Z
Learnt from: petercr
Repo: codu-code/codu PR: 1203
File: e2e/settings.spec.ts:39-41
Timestamp: 2024-11-09T15:07:15.871Z
Learning: The settings page (`app/(app)/settings/_client.tsx`) does not use a loading spinner with `data-testid="save-loading"` during the save operation.
Applied to files:
app/(app)/settings/_client.tsx
🧬 Code graph analysis (26)
components/PostEditor/extensions/s3-image-upload.ts (3)
app/actions/getUploadUrl.ts (1)
getUploadUrl(15-44)utils/s3helpers.ts (1)
uploadFile(17-28)components/editor/editor/plugins/upload-images.tsx (1)
startImageUpload(64-117)
components/PostEditor/components/TagInput.tsx (2)
components/PostEditor/index.ts (1)
TagInput(16-16)server/db/schema.ts (1)
tag(663-681)
components/PostEditor/tabs/LinkTab.tsx (3)
components/PostEditor/hooks/useLinkMetadata.ts (2)
LinkMetadata(5-11)useLinkMetadata(52-195)components/PostEditor/index.ts (4)
LinkMetadata(13-13)LinkTab(7-7)useLinkMetadata(12-12)UrlMetadataPreview(17-17)components/PostEditor/components/UrlMetadataPreview.tsx (1)
UrlMetadataPreview(19-130)
app/(app)/[username]/[slug]/_userLinkDetail.tsx (4)
server/db/schema.ts (1)
voteType(41-41)auth.ts (1)
signIn(73-85)utils/post.ts (1)
status(7-11)components/ui-components/link.tsx (1)
Link(5-25)
auth.ts (1)
server/db/schema.ts (1)
user(114-170)
app/(app)/draft/[id]/page.tsx (3)
next.config.js (1)
config(22-67)components/ui-components/link.tsx (1)
Link(5-25)markdoc/components.ts (1)
markdocComponents(7-13)
app/(app)/layout.tsx (1)
components/Layout/SidebarAppLayout.tsx (1)
SidebarAppLayout(47-53)
components/Layout/SidebarAppLayout.tsx (4)
context/SidebarContext.tsx (2)
useSidebar(46-52)SidebarProvider(20-44)components/ui-components/sidebar-layout.tsx (1)
SidebarLayout(67-129)components/SideBar/AppSidebar.tsx (1)
AppSidebar(74-190)components/Header/MinimalHeader.tsx (1)
MinimalHeader(34-207)
e2e/articles.spec.ts (2)
e2e/constants/constants.ts (1)
articleContent(1-2)e2e/utils/constants.ts (1)
articleContent(1-2)
app/(app)/articles/_client.tsx (2)
auth.ts (1)
session(65-72)server/db/schema.ts (1)
session(71-77)
components/Header/MinimalHeader.tsx (4)
context/SidebarContext.tsx (1)
useSidebar(46-52)auth.ts (1)
signIn(73-85)components/ui-components/link.tsx (1)
Link(5-25)components/ui/Search.tsx (2)
Search(517-552)MobileSearch(554-580)
app/api/fetch-metadata/route.ts (2)
utils/utils.ts (1)
isValidUrl(8-15)server/auth.ts (1)
auth(3-3)
app/(editor)/create/[[...paramsArr]]/_client.tsx (11)
server/trpc/trpc.ts (1)
router(77-77)components/PostEditor/hooks/useLinkMetadata.ts (1)
LinkMetadata(5-11)server/db/schema.ts (3)
postStatus(35-40)postType(29-34)tag(663-681)utils/post.ts (3)
PostStatus(5-5)getPostStatus(13-21)status(7-11)hooks/useDebounce.tsx (1)
useDebounce(3-15)server/trpc/react.tsx (1)
api(11-11)schema/content.ts (1)
ConfirmContentSchema(159-172)app/(editor)/create/[[...paramsArr]]/page.tsx (1)
metadata(5-28)components/PostEditor/tabs/WriteTab.tsx (1)
WriteTab(29-167)components/PostEditor/tabs/LinkTab.tsx (1)
LinkTab(27-148)components/PostEditor/components/TagInput.tsx (1)
TagInput(32-173)
components/ui-components/sidebar-layout.tsx (1)
components/ui-components/navbar.tsx (1)
NavbarItem(62-122)
components/PostEditor/PostEditor.tsx (4)
components/PostEditor/hooks/useLinkMetadata.ts (1)
LinkMetadata(5-11)components/PostEditor/tabs/WriteTab.tsx (1)
WriteTab(29-167)components/PostEditor/tabs/LinkTab.tsx (1)
LinkTab(27-148)components/PostEditor/components/TagInput.tsx (1)
TagInput(32-173)
components/PostEditor/toolbar/ArticleToolbar.tsx (2)
components/editor/editor/index.tsx (1)
Editor(17-49)components/PostEditor/index.ts (1)
ArticleToolbar(18-18)
app/(app)/[username]/[slug]/page.tsx (2)
server/db/index.ts (1)
db(30-33)server/db/schema.ts (3)
user(114-170)posts(273-356)tag(663-681)
app/(app)/page.tsx (2)
components/Hero/Hero.tsx (1)
Hero(215-245)components/PopularTags/PopularTags.tsx (1)
PopularTags(7-31)
components/PostEditor/tabs/WriteTab.tsx (2)
components/PostEditor/hooks/useArticleEditor.ts (1)
useArticleEditor(49-202)components/PostEditor/toolbar/ArticleToolbar.tsx (1)
ArticleToolbar(177-410)
server/lib/posts.ts (2)
server/db/index.ts (1)
db(30-33)server/db/schema.ts (3)
posts(273-356)user(114-170)tag(663-681)
components/PostEditor/hooks/useLinkMetadata.ts (3)
components/PostEditor/index.ts (2)
LinkMetadata(13-13)useLinkMetadata(12-12)utils/utils.ts (1)
isValidUrl(8-15)app/(editor)/create/[[...paramsArr]]/_client.tsx (2)
onError(148-151)onError(155-158)
drizzle/seed-sources.ts (2)
server/db/index.ts (1)
db(30-33)server/db/schema.ts (1)
user(114-170)
e2e/utils/utils.ts (3)
e2e/constants/constants.ts (1)
E2E_USER_ONE_ID(7-7)server/db/index.ts (1)
db(30-33)server/db/schema.ts (1)
posts(273-356)
server/api/router/content.ts (2)
schema/content.ts (1)
GetContentBySlugSchema(44-46)server/db/schema.ts (3)
user(114-170)posts(273-356)bookmarks(535-561)
app/(app)/settings/_client.tsx (4)
components/ui-components/heading.tsx (1)
Subheading(21-33)components/ui-components/text.tsx (1)
Text(4-18)components/ui-components/fieldset.tsx (2)
Field(49-67)Label(69-83)components/ui-components/divider.tsx (1)
Divider(3-20)
app/(editor)/create/[[...paramsArr]]/navigation.tsx (1)
components/ui-components/link.tsx (1)
Link(5-25)
🪛 ast-grep (0.40.4)
components/PostEditor/extensions/s3-image-upload.ts
[warning] 63-71: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: loadingOverlay.innerHTML = <div class="flex items-center gap-2 bg-white dark:bg-neutral-800 px-3 py-2 rounded-full shadow-lg"> <svg class="animate-spin h-4 w-4 text-pink-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Uploading...</span> </div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 63-71: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: loadingOverlay.innerHTML = <div class="flex items-center gap-2 bg-white dark:bg-neutral-800 px-3 py-2 rounded-full shadow-lg"> <svg class="animate-spin h-4 w-4 text-pink-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Uploading...</span> </div>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html
(unsafe-html-content-assignment)
🪛 GitHub Actions: Code Quality Checks
components/editor/editor/extensions/slash-command.tsx
[error] 240-240: Error: Calling setState synchronously within an effect can trigger cascading renders. react-hooks/set-state-in-effect
app/(editor)/create/[[...paramsArr]]/_client.tsx
[error] 198-198: Error: Calling setState synchronously within an effect can trigger cascading renders. react-hooks/set-state-in-effect
[error] 392-392: Error: Calling setState synchronously within an effect can trigger cascading renders. react-hooks/set-state-in-effect
[error] 418-418: Error: Calling setState synchronously within an effect can trigger cascading renders. react-hooks/set-state-in-effect
[error] 434-434: Error: Calling setState synchronously within an effect can trigger cascading renders. react-hooks/set-state-in-effect
🪛 GitHub Check: Run ESLint and Prettier
app/(app)/[username]/[slug]/_userLinkDetail.tsx
[warning] 192-192:
'faviconUrl' is assigned a value but never used
[warning] 192-192:
'faviconUrl' is assigned a value but never used
🪛 LanguageTool
EDITOR_SHORTCUTS.MD
[uncategorized] ~24-~24: “Windows” (operating system by Microsoft) is a proper noun and needs to be capitalized.
Context: ... to Use Hotkeys 1. Press the meta key (windows key or mac cmd key) with the desired ho...
(A_WINDOWS)
[grammar] ~25-~25: Use a hyphen to join words.
Context: ... combination and for this you can double click the word or phrase or press meta+b...
(QB_NEW_EN_HYPHEN)
[uncategorized] ~40-~40: The official name of this popular video platform is spelled with a capital “T”.
Context: ... | {% media src="url" /%} | | /youtube | Embed a YouTube video with src ...
(YOUTUBE)
[uncategorized] ~40-~40: The official name of this popular video platform is spelled with a capital “T”.
Context: ... Embed a YouTube video with src | {% youtube src="url" /%} | | /codepen |...
(YOUTUBE)
LICENSE.md
[style] ~161-~161: ‘any and all’ might be wordy. Consider a shorter alternative.
Context: ...ge, computer failure or malfunction, or any and all other commercial damages or losses)...
(EN_WORDINESS_PREMIUM_ANY_AND_ALL)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: e2e
| if (serverVoteKey !== lastSyncedKey && linkContent) { | ||
| setUserVote(currentUserVote); | ||
| setVotes({ upvotes: currentUpvotes, downvotes: currentDownvotes }); | ||
| setLastSyncedKey(serverVoteKey); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anti-pattern: Setting state directly during render.
This pattern of checking conditions and calling setState during render can cause unnecessary re-renders and is against React best practices. The sync logic should be moved to a useEffect.
🔧 Refactor to useEffect
- const serverVoteKey = `${currentUserVote}-${currentUpvotes}-${currentDownvotes}`;
- const [lastSyncedKey, setLastSyncedKey] = useState(serverVoteKey);
-
- if (serverVoteKey !== lastSyncedKey && linkContent) {
- setUserVote(currentUserVote);
- setVotes({ upvotes: currentUpvotes, downvotes: currentDownvotes });
- setLastSyncedKey(serverVoteKey);
- }
+ // Sync local state when server data changes
+ useEffect(() => {
+ if (linkContent) {
+ setUserVote(linkContent.userVote ?? null);
+ setVotes({
+ upvotes: linkContent.upvotes ?? 0,
+ downvotes: linkContent.downvotes ?? 0,
+ });
+ }
+ }, [linkContent?.userVote, linkContent?.upvotes, linkContent?.downvotes]);Don't forget to add useEffect to the imports:
-import { useState, useMemo } from "react";
+import { useState, useMemo, useEffect } from "react";Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @app/(app)/[username]/[slug]/_userLinkDetail.tsx around lines 95 - 99, Move
the conditional state updates out of render into a useEffect: import useEffect,
then create an effect that watches [serverVoteKey, lastSyncedKey, linkContent,
currentUserVote, currentUpvotes, currentDownvotes] and inside the effect check
if (serverVoteKey !== lastSyncedKey && linkContent) and then call
setUserVote(currentUserVote), setVotes({ upvotes: currentUpvotes, downvotes:
currentDownvotes }), and setLastSyncedKey(serverVoteKey); this preserves the
existing logic but avoids setting state during render and prevents unnecessary
re-renders.
| const faviconUrl = getFaviconUrl(externalUrl); | ||
| const hostname = externalUrl ? getHostname(externalUrl) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable: faviconUrl is computed but never rendered.
As flagged by ESLint, faviconUrl is assigned but never used. Either remove it or add the favicon display in the UI (e.g., next to the hostname).
🧹 Remove unused variable or use it
Option 1: Remove if not needed
- const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;Option 2: Use it in the hostname display (line 247)
- <span className="text-neutral-400">{hostname}</span>
+ <span className="flex items-center gap-1 text-neutral-400">
+ {faviconUrl && (
+ <img src={faviconUrl} alt="" className="h-4 w-4" />
+ )}
+ {hostname}
+ </span>📝 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.
| const faviconUrl = getFaviconUrl(externalUrl); | |
| const hostname = externalUrl ? getHostname(externalUrl) : null; | |
| const hostname = externalUrl ? getHostname(externalUrl) : null; |
🧰 Tools
🪛 GitHub Check: Run ESLint and Prettier
[warning] 192-192:
'faviconUrl' is assigned a value but never used
[warning] 192-192:
'faviconUrl' is assigned a value but never used
🤖 Prompt for AI Agents
In @app/(app)/[username]/[slug]/_userLinkDetail.tsx around lines 192 - 193, The
variable faviconUrl is computed via getFaviconUrl(externalUrl) but never used;
either remove that assignment or render the favicon next to the hostname in the
UI: use faviconUrl (when externalUrl is truthy) and render an <img> or Avatar
component beside where hostname (from getHostname(externalUrl)) is displayed
(refer to the hostname variable and the render around the hostname at the user
link detail JSX), ensuring you handle missing faviconUrl and set alt/title and
small size for layout.
| <> | ||
| <HeroSection /> | ||
| <MetricsSection /> | ||
| <OfferingsSection /> | ||
| <SocialProofSection /> | ||
| <ContactSection /> | ||
| </main> | ||
| </> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility concern: Removal of semantic <main> landmark.
The advertise page wrapper has been changed from a <main> element to a React fragment, removing the semantic landmark. This is the same accessibility issue observed in the username profile component.
React fragments (<>...</>) provide no semantic meaning to assistive technologies. The <main> element is an important landmark that helps users navigate to the primary content area.
If this change is part of moving the <main> element to a parent layout component (e.g., SidebarAppLayout), please ensure that:
- All pages are consistently wrapped with a single
<main>element at the layout level - Page-specific content is properly nested within that landmark
See the comment on app/(app)/[username]/_usernameClient.tsx for verification steps to check the overall landmark structure.
🤖 Prompt for AI Agents
In @app/(app)/advertise/_client.tsx around lines 13 - 19, The fragment wrapper
for the advertise page removes the semantic <main> landmark; restore
accessibility by wrapping the page sections (HeroSection, MetricsSection,
OfferingsSection, SocialProofSection, ContactSection) in a <main> element unless
a parent layout (e.g., SidebarAppLayout) already provides a single consistent
<main> for all pages—if the layout supplies it, ensure this page remains nested
inside that main; otherwise replace the fragment with a <main> to reintroduce
the primary content landmark.
| <section className="grid gap-x-8 gap-y-6 sm:grid-cols-2"> | ||
| <div className="space-y-1"> | ||
| <Subheading level={2}>Appearance</Subheading> | ||
| <Text>Choose your preferred theme</Text> | ||
| </div> | ||
| <Field className="flex items-center justify-between"> | ||
| <Label passive className="flex flex-col"> | ||
| <span>Theme</span> | ||
| <Text className="text-xs text-gray-500"> | ||
| Toggle between light and dark theme | ||
| </Text> | ||
| </Label> | ||
|
|
||
| <div className="flex items-center gap-2"> | ||
| <Sun className="h-4 w-4 text-neutral-400 dark:text-neutral-500" /> | ||
| <Switch | ||
| color="pink" | ||
| checked={resolvedTheme === "dark"} | ||
| onChange={(checked) => setTheme(checked ? "dark" : "light")} | ||
| /> | ||
| <Moon className="h-4 w-4 text-neutral-500 dark:text-neutral-400" /> | ||
| </div> | ||
| </Field> | ||
| </section> | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle resolvedTheme undefined state.
The resolvedTheme from useTheme() can be undefined during SSR or initial client render, which may cause the Switch to have an incorrect initial state or hydration mismatches.
🔧 Proposed fix to handle undefined theme
<div className="flex items-center gap-2">
<Sun className="h-4 w-4 text-neutral-400 dark:text-neutral-500" />
<Switch
color="pink"
- checked={resolvedTheme === "dark"}
+ checked={resolvedTheme === "dark" || false}
onChange={(checked) => setTheme(checked ? "dark" : "light")}
/>
<Moon className="h-4 w-4 text-neutral-500 dark:text-neutral-400" />
</div>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @app/(app)/settings/_client.tsx around lines 438 - 462, resolvedTheme from
useTheme() can be undefined during SSR/initial render causing hydration
mismatches; update the Switch checked prop to derive its value safely (e.g., use
resolvedTheme ?? theme ?? "light") so it defaults to a known value and use that
same derived value for rendering logic, e.g. replace checked={resolvedTheme ===
"dark"} with checked={(resolvedTheme ?? theme ?? "light") === "dark"} and keep
onChange={(checked) => setTheme(checked ? "dark" : "light")} so setTheme still
controls updates.
| useEffect(() => { | ||
| if (!data) return; | ||
| const { | ||
| body: existingBody, | ||
| excerpt: existingExcerpt, | ||
| title: existingTitle, | ||
| tags: existingTags, | ||
| publishedAt, | ||
| canonicalUrl: existingCanonical, | ||
| type: postType, | ||
| externalUrl, | ||
| } = data; | ||
|
|
||
| // Handle link posts vs article posts (case-insensitive check) | ||
| if (postType?.toLowerCase() === "link") { | ||
| setLinkTitle(existingTitle || ""); | ||
| setLinkUrl(externalUrl || ""); | ||
| // Only switch tab if not already on link tab (preserve URL param on initial load) | ||
| if (activeTab !== "link") { | ||
| setActiveTab("link"); | ||
| } | ||
| } else { | ||
| setTitle(existingTitle || ""); | ||
| setBody(existingBody || ""); | ||
| setCanonicalUrl(existingCanonical || ""); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (!data) return; | ||
| const { body, excerpt, title, id, tags, publishedAt } = data; | ||
| setTags(tags.map(({ tag }) => tag.title.toUpperCase())); | ||
| reset({ | ||
| body: body || "", | ||
| excerpt: excerpt || "", | ||
| title: title || "", | ||
| id, | ||
| published: publishedAt ? publishedAt : undefined, | ||
| }); | ||
| setExcerpt(existingExcerpt || ""); | ||
| setTags(existingTags.map(({ tag }) => tag.title.toUpperCase())); | ||
| setPublishedTime(publishedAt || ""); | ||
| setIsPostScheduled( | ||
| publishedAt ? new Date(publishedAt) > new Date() : false, | ||
| ); | ||
| setPostStatus( | ||
| publishedAt ? getPostStatus(new Date(publishedAt)) : status.DRAFT, | ||
| ); | ||
| }, [data, reset]); | ||
| }, [data, activeTab]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pipeline failure & potential infinite loop: setActiveTab within effect that depends on activeTab.
Line 396 calls setActiveTab("link") inside an effect with activeTab in its dependency array. This can cause cascading renders and potentially an infinite loop if conditions aren't perfectly guarded.
Suggested fix
Remove activeTab from the dependency array since the effect should only run when data changes:
- }, [data, activeTab]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data]);Or restructure to avoid conditional setActiveTab:
useEffect(() => {
if (!data) return;
// ... destructure data ...
if (postType?.toLowerCase() === "link") {
setLinkTitle(existingTitle || "");
setLinkUrl(externalUrl || "");
- if (activeTab !== "link") {
- setActiveTab("link");
- }
} else {
// ...
}
// ...
- }, [data, activeTab]);
+ }, [data]);
+ // Separate effect for initial tab sync
+ useEffect(() => {
+ if (data?.type?.toLowerCase() === "link" && activeTab !== "link") {
+ setActiveTab("link");
+ }
+ }, [data?.type]); // Only on data.type change📝 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.
| useEffect(() => { | |
| if (!data) return; | |
| const { | |
| body: existingBody, | |
| excerpt: existingExcerpt, | |
| title: existingTitle, | |
| tags: existingTags, | |
| publishedAt, | |
| canonicalUrl: existingCanonical, | |
| type: postType, | |
| externalUrl, | |
| } = data; | |
| // Handle link posts vs article posts (case-insensitive check) | |
| if (postType?.toLowerCase() === "link") { | |
| setLinkTitle(existingTitle || ""); | |
| setLinkUrl(externalUrl || ""); | |
| // Only switch tab if not already on link tab (preserve URL param on initial load) | |
| if (activeTab !== "link") { | |
| setActiveTab("link"); | |
| } | |
| } else { | |
| setTitle(existingTitle || ""); | |
| setBody(existingBody || ""); | |
| setCanonicalUrl(existingCanonical || ""); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (!data) return; | |
| const { body, excerpt, title, id, tags, publishedAt } = data; | |
| setTags(tags.map(({ tag }) => tag.title.toUpperCase())); | |
| reset({ | |
| body: body || "", | |
| excerpt: excerpt || "", | |
| title: title || "", | |
| id, | |
| published: publishedAt ? publishedAt : undefined, | |
| }); | |
| setExcerpt(existingExcerpt || ""); | |
| setTags(existingTags.map(({ tag }) => tag.title.toUpperCase())); | |
| setPublishedTime(publishedAt || ""); | |
| setIsPostScheduled( | |
| publishedAt ? new Date(publishedAt) > new Date() : false, | |
| ); | |
| setPostStatus( | |
| publishedAt ? getPostStatus(new Date(publishedAt)) : status.DRAFT, | |
| ); | |
| }, [data, reset]); | |
| }, [data, activeTab]); | |
| useEffect(() => { | |
| if (!data) return; | |
| const { | |
| body: existingBody, | |
| excerpt: existingExcerpt, | |
| title: existingTitle, | |
| tags: existingTags, | |
| publishedAt, | |
| canonicalUrl: existingCanonical, | |
| type: postType, | |
| externalUrl, | |
| } = data; | |
| // Handle link posts vs article posts (case-insensitive check) | |
| if (postType?.toLowerCase() === "link") { | |
| setLinkTitle(existingTitle || ""); | |
| setLinkUrl(externalUrl || ""); | |
| } else { | |
| setTitle(existingTitle || ""); | |
| setBody(existingBody || ""); | |
| setCanonicalUrl(existingCanonical || ""); | |
| } | |
| setExcerpt(existingExcerpt || ""); | |
| setTags(existingTags.map(({ tag }) => tag.title.toUpperCase())); | |
| setPublishedTime(publishedAt || ""); | |
| setIsPostScheduled( | |
| publishedAt ? new Date(publishedAt) > new Date() : false, | |
| ); | |
| setPostStatus( | |
| publishedAt ? getPostStatus(new Date(publishedAt)) : status.DRAFT, | |
| ); | |
| }, [data]); | |
| // Separate effect for initial tab sync | |
| useEffect(() => { | |
| if (data?.type?.toLowerCase() === "link" && activeTab !== "link") { | |
| setActiveTab("link"); | |
| } | |
| }, [data?.type]); |
🧰 Tools
🪛 GitHub Actions: Code Quality Checks
[error] 392-392: Error: Calling setState synchronously within an effect can trigger cascading renders. react-hooks/set-state-in-effect
🤖 Prompt for AI Agents
In @app/(editor)/create/[[...paramsArr]]/_client.tsx around lines 377 - 413, The
effect watches both data and activeTab but calls setActiveTab inside it, risking
cascading renders; change the dependency array to only [data] so the effect runs
when data changes (keep the existing guard if (activeTab !== "link") before
calling setActiveTab("link")), i.e., update the useEffect dependency from [data,
activeTab] to [data] while retaining the current conditional logic around
setActiveTab, referencing the useEffect, activeTab, setActiveTab, data, and
postType symbols.
| const isExternal = | ||
| props.href.startsWith("http") || props.href.startsWith("//"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete protocol handling for special URL schemes.
The isExternal check only covers http and // protocols, but doesn't handle other URL schemes like mailto:, tel:, ftp:, data:, etc. These would be incorrectly treated as internal links and could cause navigation issues. For example, clicking a mailto: link would attempt Next.js routing instead of opening an email client.
🔧 Proposed fix for robust URL scheme detection
const isExternal =
- props.href.startsWith("http") || props.href.startsWith("//");
+ props.href.startsWith("http://") ||
+ props.href.startsWith("https://") ||
+ props.href.startsWith("//") ||
+ props.href.startsWith("mailto:") ||
+ props.href.startsWith("tel:");Or for a more comprehensive approach:
const isExternal =
- props.href.startsWith("http") || props.href.startsWith("//");
+ /^(https?:\/\/|\/\/|mailto:|tel:|ftp:)/.test(props.href);📝 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.
| const isExternal = | |
| props.href.startsWith("http") || props.href.startsWith("//"); | |
| const isExternal = | |
| props.href.startsWith("http://") || | |
| props.href.startsWith("https://") || | |
| props.href.startsWith("//") || | |
| props.href.startsWith("mailto:") || | |
| props.href.startsWith("tel:"); |
| const isExternal = | |
| props.href.startsWith("http") || props.href.startsWith("//"); | |
| const isExternal = | |
| /^(https?:\/\/|\/\/|mailto:|tel:|ftp:)/.test(props.href); |
| After making changes make sure that tests passes | ||
|
|
||
| To create a e2e test make a file in `/e2e` directory | ||
| To create a e2e test make a file in `/e2e` directory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix article usage in grammar.
Line 123 uses "a e2e" but should use "an e2e" before vowel sounds.
📝 Proposed fix
-To create a e2e test make a file in `/e2e` directory
+To create an e2e test make a file in `/e2e` directory📝 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.
| To create a e2e test make a file in `/e2e` directory | |
| To create an e2e test make a file in `/e2e` directory |
🤖 Prompt for AI Agents
In @CONTRIBUTING.md at line 123, The phrase "To create a e2e test make a file in
`/e2e` directory" uses the wrong article; update the sentence in CONTRIBUTING.md
so the phrase reads "To create an e2e test make a file in `/e2e` directory"
(replace "a e2e" with "an e2e") to correct the grammar.
|
|
||
| ## How to Use Hotkeys | ||
|
|
||
| 1. Press the meta key (windows key or mac cmd key) with the desired hotkey eg, cmd+1 to render # |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capitalize proper noun "Windows."
"Windows" is a registered trademark and proper noun, not a common noun.
📝 Proposed fix
-1. Press the meta key (windows key or mac cmd key) with the desired hotkey eg, cmd+1 to render #
+1. Press the meta key (Windows key or Mac cmd key) with the desired hotkey eg, cmd+1 to render #📝 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.
| 1. Press the meta key (windows key or mac cmd key) with the desired hotkey eg, cmd+1 to render # | |
| 1. Press the meta key (Windows key or Mac cmd key) with the desired hotkey eg, cmd+1 to render # |
🧰 Tools
🪛 LanguageTool
[uncategorized] ~24-~24: “Windows” (operating system by Microsoft) is a proper noun and needs to be capitalized.
Context: ... to Use Hotkeys 1. Press the meta key (windows key or mac cmd key) with the desired ho...
(A_WINDOWS)
🤖 Prompt for AI Agents
In @EDITOR_SHORTCUTS.MD at line 24, Update the sentence that reads "Press the
meta key (windows key or mac cmd key) with the desired hotkey eg, cmd+1 to
render #" to capitalize the proper noun "Windows" (i.e., change "windows key" to
"Windows key") and ensure "mac" is styled consistently (e.g., "Mac" or "macOS")
if desired for consistency with project style.
| ## How to Use Hotkeys | ||
|
|
||
| 1. Press the meta key (windows key or mac cmd key) with the desired hotkey eg, cmd+1 to render # | ||
| 2. You can also highlight the word and then use the hotkey combination and for this you can double click the word or phrase or press meta+backspace a few times to highlight the required selection of text. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add hyphen to compound adjective "double-click."
Compound adjectives preceding a noun should be hyphenated per standard English conventions.
📝 Proposed fix
-2. You can also highlight the word and then use the hotkey combination and for this you can double click the word or phrase or press meta+backspace a few times to highlight the required selection of text.
+2. You can also highlight the word and then use the hotkey combination and for this you can double-click the word or phrase or press meta+backspace a few times to highlight the required selection of text.📝 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.
| 2. You can also highlight the word and then use the hotkey combination and for this you can double click the word or phrase or press meta+backspace a few times to highlight the required selection of text. | |
| 2. You can also highlight the word and then use the hotkey combination and for this you can double-click the word or phrase or press meta+backspace a few times to highlight the required selection of text. |
🧰 Tools
🪛 LanguageTool
[grammar] ~25-~25: Use a hyphen to join words.
Context: ... combination and for this you can double click the word or phrase or press meta+b...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
In @EDITOR_SHORTCUTS.MD at line 25, Update the sentence in EDITOR_SHORTCUTS.MD
to hyphenate the compound adjective by changing "double click the word" to
"double-click the word" so the phrase correctly reads "...and for this you can
double-click the word or phrase or press meta+backspace..." ensuring compound
adjective style is consistent.
| | Shortcut | Description | Example | | ||
| | -------------- | ------------------------------------ | ------------------------------ | | ||
| | `/media` | Embed a media file with src | `{% media src="url" /%}` | | ||
| | `/youtube` | Embed a YouTube video with src | `{% youtube src="url" /%}` | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capitalize proper noun "YouTube."
"YouTube" is a registered trademark with specific capitalization; ensure both instances are corrected (line 40 appears twice in the table row).
📝 Proposed fix
-| `/youtube` | Embed a YouTube video with src | `{% youtube src="url" /%}` |
+| `/youtube` | Embed a YouTube video with src | `{% YouTube src="url" /%}` |Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 LanguageTool
[uncategorized] ~40-~40: The official name of this popular video platform is spelled with a capital “T”.
Context: ... | {% media src="url" /%} | | /youtube | Embed a YouTube video with src ...
(YOUTUBE)
[uncategorized] ~40-~40: The official name of this popular video platform is spelled with a capital “T”.
Context: ... Embed a YouTube video with src | {% youtube src="url" /%} | | /codepen |...
(YOUTUBE)
🤖 Prompt for AI Agents
In @EDITOR_SHORTCUTS.MD at line 40, Update the table row to capitalize the
proper noun "YouTube" in both places: change the command label `/youtube` to
`/YouTube` and the shortcode example `{% youtube src="url" /%}` to `{% YouTube
src="url" /%}` so both occurrences use the correct capitalization.
…ling and state management
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
app/(editor)/create/[[...paramsArr]]/_client.tsx (4)
106-128: Consider consolidating related state with useReducer.With 15+ useState declarations, the component's state management is becoming complex. Related state (form fields, UI flags, publishing options) could be grouped into a reducer for better maintainability and clearer state transitions.
Example refactor pattern
type EditorState = { form: { title: string; body: string; linkUrl: string; linkTitle: string; // ... other form fields }; ui: { viewPreview: boolean; showPublishConfirm: boolean; // ... other UI state }; // ... other state groups }; const [state, dispatch] = useReducer(editorReducer, initialState);This would make state updates more predictable and easier to test.
183-183: Consider memoizing PREVIEW_URL.The preview URL is reconstructed on every render. While this is a simple string operation, memoizing it would follow React best practices and prevent unnecessary recalculations.
Suggested refactor
- const PREVIEW_URL = `${process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://www.codu.co"}/draft/${createData?.id || postId}`; + const PREVIEW_URL = useMemo( + () => `${process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://www.codu.co"}/draft/${createData?.id || postId}`, + [createData?.id, postId] + );
478-491: Consider refactoring unsaved changes tracking.The
eslint-disablecomments forset-state-in-effectindicate a code smell. Setting state directly in effects based on other state values can cause unnecessary re-renders and is generally discouraged. Consider tracking unsaved changes by comparing current form values to saved values in a useMemo, or using refs to track the last saved state.Alternative approach
// Track last saved state with refs const lastSavedStateRef = useRef({ title: '', body: '', linkUrl: '', linkTitle: '' }); // Compute unsaved changes as derived state const hasUnsavedChanges = useMemo(() => { if (activeTab === "write") { return title !== lastSavedStateRef.current.title || body !== lastSavedStateRef.current.body; } else { return linkUrl !== lastSavedStateRef.current.linkUrl || linkTitle !== lastSavedStateRef.current.linkTitle; } }, [title, body, linkUrl, linkTitle, activeTab]); // Update ref after successful save const savePost = async () => { // ... save logic lastSavedStateRef.current = { title, body, linkUrl, linkTitle }; };This eliminates the effects and the need for eslint-disable.
743-750: Consider memoizing Markdoc rendering.The Markdoc parse/transform/render pipeline runs on every render. Memoizing this operation would prevent unnecessary reprocessing when other state changes.
Performance optimization
+ const renderedContent = useMemo(() => { + if (!body) return null; + return Markdoc.renderers.react( + Markdoc.transform(Markdoc.parse(body), config), + React, + { components: markdocComponents } + ); + }, [body]); + <article ...> <h1>{title || "Untitled"}</h1> - {Markdoc.renderers.react( - Markdoc.transform(Markdoc.parse(body), config), - React, - { components: markdocComponents } - )} + {renderedContent} </article>
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/(editor)/create/[[...paramsArr]]/_client.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-22T08:43:13.236Z
Learnt from: dineshsutihar
Repo: codu-code/codu PR: 1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.236Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.
Applied to files:
app/(editor)/create/[[...paramsArr]]/_client.tsx
🧬 Code graph analysis (1)
app/(editor)/create/[[...paramsArr]]/_client.tsx (9)
components/PostEditor/PostEditor.tsx (1)
PostType(10-10)components/PostEditor/hooks/useLinkMetadata.ts (1)
LinkMetadata(5-11)server/trpc/react.tsx (1)
api(11-11)utils/removeMarkdown.ts (1)
removeMarkdown(11-123)schema/content.ts (1)
ConfirmContentSchema(159-172)app/(editor)/create/[[...paramsArr]]/page.tsx (1)
metadata(5-28)components/PostEditor/tabs/WriteTab.tsx (1)
WriteTab(29-167)components/PostEditor/tabs/LinkTab.tsx (1)
LinkTab(27-148)components/PostEditor/components/TagInput.tsx (1)
TagInput(32-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: e2e
🔇 Additional comments (17)
app/(editor)/create/[[...paramsArr]]/_client.tsx (17)
1-81: LGTM! Clean structure for tab configuration.The imports are well-organized, and the TAB_CONFIG provides a maintainable way to define tab metadata. The PostType definition is properly typed.
84-104: Excellent URL state synchronization.The tab state is properly synchronized with the URL query parameter, enabling deep linking and browser history navigation. Using
router.replacewithscroll: falseis the correct approach to avoid unnecessary navigation stack pollution.
130-134: LGTM! Smart tab locking prevents data loss.The tab locking logic correctly prevents users from switching tabs when they have content in the current tab, avoiding accidental data loss. The implementation is clear and user-friendly.
178-180: Verify aggressive cache strategy.Setting
staleTime: Infinitymeans the draft data will never be considered stale and won't refetch even if it changes elsewhere (e.g., another browser tab, collaborative editing). While this might be intentional for single-editor scenarios, consider whether this could cause stale data issues.If the draft can only be edited in one place at a time, this is fine. Otherwise, consider a reasonable staleTime (e.g., 30000 for 30 seconds) or rely on TRPC's default caching behavior.
201-205: LGTM! Valid pattern for deferred state updates.Using
queueMicrotaskto defer the state update avoids potential synchronous setState issues during effect execution. This is a valid workaround for React's constraints, though it does indicate the component is managing complex async state.
213-224: LGTM! Clean data extraction pattern.The
getFormDatacallback is properly memoized and handles excerpt generation fallback cleanly.
227-279: LGTM! Comprehensive save logic handling both tabs.The
savePostfunction correctly handles both create and update scenarios, differentiating between write and link content types. Returning the post ID enables the publish flow to work with newly created posts. The complexity is justified by the feature requirements.
291-379: LGTM! Robust validation and publish flow.Excellent approach validating content before saving (preventing invalid drafts) and using the returned post ID for publish (critical for newly created posts). The handling of Markdoc syntax errors, scheduling, and redirects is comprehensive and correct.
383-423: LGTM! Proper form hydration with safeguards.The ref-based guard ensures form data populates exactly once, and the
queueMicrotaskpattern defers updates to avoid synchronous setState issues. The type-based branching correctly handles both link and article posts.
435-460: LGTM! Auto-save logic properly handles both tab types.The effect correctly implements per-tab auto-save requirements and uses
queueMicrotaskto defer the save operation, avoiding synchronous state updates. The extensive dependency array is necessary given the conditions being checked.
463-467: LGTM! Proper redirect handling with state preservation.The redirect after post creation correctly preserves the active tab in the URL, maintaining a consistent user experience.
546-622: LGTM! Delightful publish confirmation dialog.The pre-publish dialog provides excellent UX with context-aware messaging, emoji, and button text that adapts to the post type (write/link) and scheduling state. The Headless UI implementation ensures proper accessibility.
646-729: LGTM! Polished tab bar with excellent UX.The animated tab indicator using framer-motion provides smooth visual feedback. The disabled state handling with tooltips clearly communicates why tabs are locked, and the Share Draft / Preview controls are well-positioned for quick access.
755-776: LGTM! Clean tab component composition.The conditional rendering of WriteTab and LinkTab with properly typed props and memoized callbacks demonstrates good component architecture.
790-919: LGTM! Well-organized progressive disclosure.The "More Options" accordion appropriately shows write-specific options (SEO, scheduling) and provides clear, helpful UI for excerpt editing, post scheduling, and draft sharing.
922-952: LGTM! Intuitive action bar with contextual controls.The action bar correctly shows Save Draft only for the write tab and provides contextual button text based on the post's publish status. Disabled states prevent invalid actions.
959-971: LGTM! Proper Suspense boundary for App Router.The Suspense wrapper is necessary for
useSearchParamsin Next.js 15 App Router, and the loading fallback provides good UX during initialization.
Create a new layout for the platform.