Conversation
- Removed Embed model and related fields from Source model in Prisma schema. - Updated Source model to include a JSON field for message content. - Refactored data handling in source store provider and state management. - Simplified getSources and getSource functions to exclude unnecessary includes. - Updated default welcome and goodbye messages to use new embed structure. - Removed unused embed-related functions and types from dal.ts and state management. - Updated package dependencies for discord.js builders and added prisma-json-types-generator. - Adjusted TypeScript configuration to include new prisma types.
…ody for better compatibility
- Update embed handling in various components to support modified message state. - Introduce new actions in the store for adding, editing, moving, and deleting embeds and fields. - Ensure proper handling of embed properties such as title, description, color, and author details. - Improve validation for embed fields and ensure default values are set where necessary. - Adjust UI components to reflect changes in embed state management.
refactor: improve message parsing and embed rendering in MessagePreview component fix: ensure proper handling of missing SourceStore context in EmbedsViewer cleanup: remove unused schema types and optimize imports in text parsing
…idation in updateSource function
…g in updateSource function
…ed code fix: update SaveButton to reset modified state correctly fix: improve channel ID handling in SendMenu component fix: enhance error handling in EnableModuleButton fix: update createSource function to handle errors gracefully chore: update Next.js to version 15.5.4 and adjust dependencies refactor: update imports in image state management
- Simplified embed state handling by removing the 'modified' state and directly updating the 'message' state. - Updated various components to reflect changes in state management, ensuring they access the correct properties. - Improved performance by reducing unnecessary state copies and checks. - Enhanced error handling for missing context providers. - Cleaned up code for better readability and maintainability.
- Updated import paths for various components to follow a consistent naming convention and directory structure. - Replaced deprecated or incorrect component imports with their new paths. - Removed unused components and files related to the dashboard and guild features. - Introduced new components for better encapsulation and reusability, such as AccordionItemWrapper and EmbedsAccordionWrapper. - Added logging for permission checks in the guild permissions utility. - Implemented a placeholder Image Editor component for future development.
- Implemented image preview feature in the main editor, allowing users to position images either inside embeds or outside. - Added state management for image positioning in `state/source.ts`. - Created `ImagePositionEditor` component for selecting image placement. - Updated main editor to integrate the new image position editor. - Enhanced message parsing to support image display based on selected position. - Developed real-time image preview in `message-preview.tsx` and `message-context-preview.tsx`. - Introduced `BackgroundEditor`, `JsonEditor`, `TextEditor`, and `Toolbar` components for comprehensive image editing capabilities. - Established Zustand store for managing image card state and actions. - Added proxy middleware for session management in protected routes. - Ensured backward compatibility and no breaking changes in existing API.
…sx, and imageCardStoreProvider.tsx
- Updated imports from "@heroui/react" to specific component packages such as "@heroui/button", "@heroui/card", "@heroui/tooltip", etc. - Implemented parallel data fetching in the dashboard layout for improved performance. - Replaced loading states with skeleton components in stats viewer for better user experience. - Refactored sidebar components to improve structure and maintainability. - Enhanced form handling in module buttons to utilize `useFormStatus` for loading states.
fix image position not saving when saving from embed to outside
…oject structure - Updated import paths for Prisma client and enums to use the new generated structure. - Added generated directories to .gitignore. - Modified various components and pages to accommodate the new import paths. - Updated package.json to reflect new dependencies and versions. - Removed outdated ERD documentation. - Adjusted Prisma configuration for better organization and clarity.
- Added avatar border color editor with preset colors and custom color picker. - Implemented background editor with solid color and image URL options, including preset gradients. - Improved preview component with loading and error states. - Refactored text editor to include variable hints for dynamic text. - Removed unused toolbar component and integrated save functionality into a new SaveButton component. - Introduced EditorHeader for better navigation and context. - Updated hooks to optimize image generation and state management.
…e management for images
…dling in preview component
…dling in preview component
… improving import paths
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
✨ Finishing touches🧪 Generate unit tests (beta)
Tip 🧪 Unit Test Generation v2 is now available!We have significantly improved our unit test generation capabilities. To enable: Add this to your reviews:
finishing_touches:
unit_tests:
enabled: trueTry it out by using the Have feedback? Share your thoughts on our Discord thread! 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 |
…gnment Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…te for accessibility in text component
There was a problem hiding this comment.
Actionable comments posted: 5
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/auth/error/page.tsx (1)
10-11: Typo in user-facing error message."as occured" should be "has occurred".
Proposed fix
let error_description = - params.error_description ?? "An unknown error as occured"; + params.error_description ?? "An unknown error has occurred";components/dashboard/guild/editor/embed/fields/clear-embed-fields-button.tsx (1)
27-28: Button label is incorrect - says "Clear Embeds" but clears fields.The button calls
clearFields(embedIndex)but the label says "Clear Embeds". This is misleading to users.🐛 Suggested fix
onPress={() => clearFields(embedIndex)} > - Clear Embeds + Clear Fields </Button>components/dashboard/guild/editor/editor.tsx (1)
12-35: Handle unauthenticated user instead of non-null assertion.Line 35 uses
user!, butgetUser()can returnnull. This will throw and crash the editor for unauthenticated sessions. Please guard and render a fallback or redirect before callingtoObject().🛠️ Suggested guard
export async function Editor({ guild }: { guild: Guild }) { const user = await getUser(); + if (!user) { + // TODO: redirect to login or render an error state + return null; + } const channels = await guild.getChannels(); return ( @@ <EditorMessagePreview guild={guild.toObject()} - user={user!.toObject()} + user={user.toObject()} />
🤖 Fix all issues with AI agents
In `@components/dashboard/guild/editor/message-preview.tsx`:
- Line 7: The import line in message-preview.tsx currently imports useMemo and
useState from React but is missing useEffect; update the import statement (the
line containing "import {useMemo, useState} from \"react\";") to also include
useEffect so the MessagePreview component (and any DOM access logic using
useEffect) can use it.
- Around line 25-28: The render body in message-preview.tsx currently calls
document.getElementById("preview-canvas") and setCanvasElement directly, causing
infinite re-renders and SSR issues; move that DOM access into a useEffect (or
useLayoutEffect) that runs on mount and/or when dependencies change, or better
yet attach a React ref to the canvas and call setCanvasElement(ref.current)
inside the effect; ensure you remove direct document access from the component
body, guard any document use with typeof document !== "undefined" for SSR, and
update references to preview-canvas, htmlCanvasElement, and setCanvasElement
accordingly.
In `@next.config.ts`:
- Around line 20-24: You are mixing ES module export (export default nextConfig)
with CommonJS (module.exports = ...); replace the CommonJS line so the Sentry
wrapper is applied to the ES export instead: call withSentryConfig with
nextConfig (not module.exports) and export the wrapped config as the default
export (i.e., export default withSentryConfig(nextConfig, {...})), and remove
the module.exports usage; ensure withSentryConfig is imported/available before
use.
In `@prisma/schema.prisma`:
- Around line 6-8: The Prisma datasource block named `db` is missing the
required `url` field; update the `datasource db` definition to include a `url`
attribute (for example using an environment variable like env("DATABASE_URL") or
a full connection string) so Prisma commands such as `prisma validate` and
`prisma generate` succeed; modify the `datasource db` block in `schema.prisma`
to add the `url` entry alongside the existing `provider` entry.
In `@proxy.ts`:
- Line 5: The middleware won't run because the file is named proxy.ts; rename
the file to middleware.ts so Next.js recognizes and executes the exported
handler (export default async function proxy). After renaming, verify the
existing export default async function proxy and any exported config/matcher
remain in the file (no other changes needed) so Next.js can pick up the
middleware.
🟠 Major comments (23)
instrumentation-client.ts-26-28 (1)
26-28: Privacy compliance risk withsendDefaultPii: true.Enabling
sendDefaultPiicauses Sentry to automatically capture user IP addresses, cookies, user-agent strings, and potentially other personally identifiable information. This requires:
- Explicit disclosure in your privacy policy
- Proper user consent mechanisms under GDPR/CCPA
- Consideration of data retention policies
Verify this aligns with your compliance requirements before deploying to production.
sentry.server.config.ts-16-18 (1)
16-18: EnablingsendDefaultPiihas compliance implications.Setting
sendDefaultPii: truewill cause Sentry to automatically collect and transmit user IP addresses, user IDs, usernames, and other personally identifiable information. This has GDPR/CCPA compliance implications and may require:
- Updating your privacy policy to disclose this data collection
- Ensuring appropriate data processing agreements with Sentry
- Configuring data retention policies in Sentry
Verify this is intentional and that appropriate compliance measures are in place.
sentry.edge.config.ts-17-19 (1)
17-19: Same PII compliance concern as server config.The
sendDefaultPii: truesetting here mirrors the server config. Ensure compliance measures (privacy policy updates, data processing agreements) cover both server and edge runtime contexts.components/dashboard/guild/manage-button.tsx-1-20 (1)
1-20: Fix route derivation logic;DMenum breaks to/droute.The
DMSourceType enum value becomes/dviaslice(0, -1).toLowerCase(), but no corresponding route exists. While the logic currently works forWelcomer→welcomeandLeaver→leave, this approach is fragile. Use an explicit mapping to handle all SourceType variants correctly.Proposed fix (explicit mapping)
- const link = `/dashboard/${guildId}/${module.slice(0, -1).toLowerCase()}`; + const moduleRoute: Record<SourceType, string> = { + Welcomer: "welcome", + Leaver: "leave", + DM: "dm", + }; + const link = `/dashboard/${guildId}/${moduleRoute[module]}`;app/dashboard/[guildId]/leave/layout.tsx-18-25 (1)
18-25: Pass the correcttypein the fallback initialState.When no source exists, the fallback
{ guildId }is missing thetypefield. The store merges withdefaultState(which hardcodestype: "Welcomer"), causing the Leaver layout to incorrectly initialize with Welcome type. This breaks theaddEmbedaction, which selectsdefaultWelcomeEmbedinstead ofdefaultLeaverEmbedbased onstate.type.Provide the module type in the fallback:
initialState={ source && source[0] ? (source[0] as Source) : { guildId: guildId, type: "Leaver", } }Apply the same fix to the welcome layout with
type: "Welcomer".components/dashboard/guild/editor/embed/footer/embed-footer-text-input.tsx-11-35 (1)
11-35: Guard against undefined embed before callingeditEmbed.
...embed!will throw ifembedis missing (e.g., out-of-range index or not yet loaded). Add a guard to prevent runtime crashes during user input.🔧 Proposed fix
- onValueChange={(value) => - editEmbed(embedIndex, { - ...embed!, - footer: { - ...embed?.footer, - text: value || "", - icon_url: embed?.footer?.icon_url || "", // Ensure 'icon_url' is always defined - }, - }) - } + onValueChange={(value) => { + if (!embed) return; + editEmbed(embedIndex, { + ...embed, + footer: { + ...embed.footer, + text: value || "", + icon_url: embed.footer?.icon_url || "", // Ensure 'icon_url' is always defined + }, + }); + }}components/dashboard/guild/editor/embed/body/embed-body-color-input.tsx-11-27 (1)
11-27: Avoid spreading undefined embed in color updates.
Line 23 spreadsembed, which can throw if the embed is missing.Suggested fix
onValueChange={(value) => editEmbed(embedIndex, { - ...embed, + ...(embed ?? {}), color: value ? parseInt(value.replace("#", ""), 16) : undefined, }) }components/dashboard/guild/editor/embed/body/embed-body-title.tsx-12-27 (1)
12-27: Prevent runtime error whenembedis undefined.
Line 23 spreadsembed, which throws if the embed is missing (stale index / removed).Suggested fix
onValueChange={(value) => editEmbed(embedIndex, { - ...embed, + ...(embed ?? {}), title: value, }) }components/dashboard/guild/editor/embed/footer/embed-footer-icon-input.tsx-21-29 (1)
21-29: Non-null assertion on potentially undefinedembedcan cause runtime error.Line 23 uses
embed!butembedcan be undefined based on the selector at line 11 (state.message?.embeds?.[embedIndex]). IfonValueChangefires whenembedis undefined, this will throw.🛡️ Proposed fix to guard against undefined embed
onValueChange={(value) => + embed && editEmbed(embedIndex, { - ...embed!, + ...embed, footer: { ...embed?.footer, icon_url: value || undefined, text: embed?.footer?.text || "", // Ensure 'text' is always defined }, }) }Alternatively, disable the input or return early if
embedis undefined.components/dashboard/guild/image-editor/hooks/use-image-editor.ts-11-31 (1)
11-31: Reset loading state on failure (currently stuck on error).
isLoadingis only cleared on success. IfgenerateImagethrows, the UI remains in a loading state.🔧 Proposed fix
const timeoutId = setTimeout(() => { const fetchCard = async () => { - try { - console.log("Generating image..."); - setIsLoading(true); - setError(null); - - await generateImage(config, guildId).then( - () => { - setIsLoading(false); - } - ) - console.log("Image generated successfully!"); - } catch (error) { + setIsLoading(true); + setError(null); + try { + console.log("Generating image..."); + await generateImage(config, guildId); + console.log("Image generated successfully!"); + } catch (error) { console.error("Error generating image:", error); setError( error instanceof Error ? error.message : "Failed to generate image", ); + } finally { + setIsLoading(false); } };components/shared/status/guild-input.tsx-42-46 (1)
42-46: Bug: ConditionshardId !== undefinedalways evaluates to true.Since
shardIdis typed asnumber | nulland initialized tonull, the conditionshardId !== undefinedwill always be true. This causes "Selected Shard ID: null" to render immediately on mount.🐛 Proposed fix
- {shardId !== undefined && ( + {shardId !== null && ( <div className="mt-2"> <p className="text-sm text-gray-500">Selected Shard ID: {shardId}</p> </div> )}utils/formatter.ts-13-23 (1)
13-23: Missing embed fields will be silently dropped.The
formatDiscordEmbedfunction omits severalAPIEmbedfields:author,fields,provider, andvideo. Any embeds containing these properties will lose that data when passed through this formatter.🐛 Proposed fix to include missing fields
export const formatDiscordEmbed = (embed: APIEmbed):APIEmbed => { return { title: embed.title ?? undefined, description: embed.description ?? undefined, url: embed.url ?? undefined, timestamp: embed.timestamp ?? undefined, color: embed.color ?? undefined, footer: embed.footer ?? undefined, image: embed.image ?? undefined, thumbnail: embed.thumbnail ?? undefined, + author: embed.author ?? undefined, + fields: embed.fields?.length ? embed.fields : undefined, + video: embed.video ?? undefined, + provider: embed.provider ?? undefined, } }lib/discord/text.tsx-50-70 (1)
50-70:{user}placeholders will leak into embed text.
parseTextonly replaces{user}when theimageflag is true, but embed titles/descriptions/fields/footers callparseTextwithout that flag. Those placeholders will render verbatim inside embeds. If embeds should still substitute{user}, pass the flag (or introduce a clearer option) for embed parsing.🔧 Suggested fix (apply consistently to all embed text fields)
- embedTitle={parseText(embed.title, user, guild)} + embedTitle={parseText(embed.title, user, guild, true)} ... - {parseText(embed.description, user, guild)} + {parseText(embed.description, user, guild, true)} ... - name: parseText(field.name, user, guild), - value: parseText(field.value, user, guild), + name: parseText(field.name, user, guild, true), + value: parseText(field.value, user, guild, true), ... - {parseText(embed.footer.text, user, guild)} + {parseText(embed.footer.text, user, guild, true)}components/dashboard/guild/editor/save-button.tsx-93-115 (1)
93-115: Avoid clearing dirty state on in-flight edits and always reset loading.If the user edits while the save request is in flight,
lastSavedStateRefis set to a stale snapshot andhasChangesis cleared even though the current state diverged. Also, exceptions fromupdateSourceleaveisLoadingstuck. Capture a save snapshot, compare after completion, and reset loading in afinally.🛠️ Suggested fix
onPress={async () => { - setIsLoading(true); - const { done, error } = await updateSource({ + setIsLoading(true); + const saveSnapshot = currentStateStr; + try { + const { done, error } = await updateSource({ guildId: state.guildId, id: state.id, channelId: state.channelId, message: state.message, imagePosition: state.imagePosition, imageEmbedIndex: state.imageEmbedIndex, - }); - if (error) { - console.error(error); - toast.error(error); - } else if (done) { - toast.success("Settings updated successfully!"); - // Update the last saved state - lastSavedStateRef.current = currentStateStr; - setHasChanges(false); - } - setIsLoading(false); + }); + if (error) { + console.error(error); + toast.error(error); + } else if (done) { + toast.success("Settings updated successfully!"); + const latest = store.getState(); + const latestStateStr = JSON.stringify({ + channelId: latest.channelId, + message: latest.message, + imagePosition: latest.imagePosition, + imageEmbedIndex: latest.imageEmbedIndex, + }); + if (latestStateStr === saveSnapshot) { + lastSavedStateRef.current = latestStateStr; + setHasChanges(false); + } + } + } catch (err) { + console.error(err); + toast.error("Failed to save changes."); + } finally { + setIsLoading(false); + } }}lib/discord/text.tsx-82-108 (1)
82-108: Reorder replacements to process code blocks before mentions to prevent broken formatting.When
{user}is replaced before code blocks and inline code are processed, the string splits into an array of elements. Subsequent regex patterns for code formatting can no longer match across the split boundaries. For example,`{user}`would be broken into separate array elements, preventing the backticks from being recognized as code delimiters.Process code formatting (multiline and inline) first while the text is still a complete string, then apply mention replacements.
Suggested reordering
- let replacedText: React.ReactNode[] | string = reactStringReplace( - processedText, - /(\{user\})/g, - (match, i) => ( - <DiscordMention highlight key={`user-${i}`}> - {user.username} - </DiscordMention> - ), - ); - - // Replace multiline code blocks (```code```) - replacedText = reactStringReplace( + let replacedText: React.ReactNode[] | string = reactStringReplace( + processedText, + /```([\s\S]+?)```/g, + (match, i) => ( + <DiscordCode multiline key={match + i}> + {match} + </DiscordCode> + ), + ); + + // Replace inline code (`code`) + replacedText = reactStringReplace(replacedText, /`([^`]+)`/g, (match, i) => ( + <DiscordCode key={match + i}>{match}</DiscordCode> + )); + + // Replace {user} with a mention after code spans are handled + replacedText = reactStringReplace( + replacedText, + /(\{user\})/g, + (match, i) => ( + <DiscordMention highlight key={`user-${i}`}> + {user.username} + </DiscordMention> + ), + );providers/imageCardStoreProvider.tsx-22-31 (1)
22-31: ReplaceuseMemowithuseReffor store initialization to prevent unwanted store recreation.The current implementation recreates the store whenever
initialStateidentity changes. Since the layout components pass conditionally-created objects, each re-render triggers a new store instance, wiping any in-flight edits. UseuseRefwith lazy initialization instead—this is the recommended Zustand pattern and ensures a single stable store instance per provider.♻️ Suggested change
-import { createContext, useContext, useMemo, type ReactNode } from "react"; +import { createContext, useContext, useRef, type ReactNode } from "react"; @@ - const store = useMemo( - () => createImageCardStore(initialState), - [initialState] - ); + const storeRef = useRef<ImageCardStoreAPI>(); + if (!storeRef.current) { + storeRef.current = createImageCardStore(initialState); + } + const store = storeRef.current;lib/discord/image.ts-3-12 (1)
3-12: Removecache()wrapper from side-effect function.
cache()memoizes results and skips re-execution for identical arguments—it is designed for idempotent operations (data fetching, computations) and explicitly discouraged for side effects. ThegenerateImagefunction mutates the DOM (renders to canvas), so identical configs will cause the canvas to skip re-rendering, producing stale previews. Additionally,cache()is intended for Server Components; using it in a client component ("use client") may not work as intended.🧹 Suggested change
-import {cache} from "react"; +// import {cache} from "react"; @@ -export const generateImage = cache( - async ( - msg: BaseCardConfig, - guildId: string, - ): Promise<void> => { +export const generateImage = async ( + msg: BaseCardConfig, + guildId: string, +): Promise<void> => { @@ - }, -); +};state/source.ts-51-60 (1)
51-60: Avoid mutating shared default embed/field objects.
defaultWelcomeEmbed,defaultLeaverEmbed,defaultEmbed, anddefaultEmbedFieldare shared references; once pushed into state, later edits mutate the constants and affect future defaults. Clone before inserting.🛠️ Suggested fix
+const clone = <T,>(value: T): T => structuredClone(value); + const defaultState: SourceState = { id: 0, activeCard: undefined, guildId: "", type: "Welcomer", channelId: "", message: { - content: "", - embeds: [defaultEmbed], + content: "", + embeds: [clone(defaultEmbed)], }, activeCardId: null, createdAt: null, updatedAt: null, }; @@ addEmbed: (embed) => set((state) => { state.message = state.message ?? { embeds: [] }; state.message.embeds = state.message.embeds ?? []; if (state.type === "Welcomer" && !embed) { - embed = defaultWelcomeEmbed; + embed = clone(defaultWelcomeEmbed); } else if (state.type === "Leaver" && !embed) { - embed = defaultLeaverEmbed; + embed = clone(defaultLeaverEmbed); } - state.message.embeds.push(embed ?? defaultEmbed); + state.message.embeds.push(clone(embed ?? defaultEmbed)); }), @@ addField: (embedIndex) => set((state) => { const embed = state.message?.embeds?.[embedIndex]; if (!embed) { return; } embed.fields = embed.fields ?? []; - embed.fields.push(defaultEmbedField); + embed.fields.push(clone(defaultEmbedField)); }),Also applies to: 98-109, 158-166
lib/dal.ts-165-168 (1)
165-168:createModuleStatsdoesn’t await async work.
forEachwithasyncwon’t be awaited, socreateSourcecan return before stats are created. UsePromise.allwithmap.✅ Suggested fix
export async function createModuleStats(guildId: string, source: SourceType) { - Object.values(Period).forEach(async (period) => { - await createGuildStats(guildId, period, source); - }); + await Promise.all( + Object.values(Period).map((period) => + createGuildStats(guildId, period, source), + ), + ); }state/source.ts-110-156 (1)
110-156: KeepimageEmbedIndexin sync when embeds move/delete.Reordering or deleting embeds can leave
imageEmbedIndexpointing to the wrong embed (or out of bounds). Consider recomputing it after each embed mutation that changes indices.🛠️ Suggested fix (helper + calls)
- immer<SourceStore>((set, get, store) => { + immer<SourceStore>((set, get, store) => { + const syncImageEmbedIndex = (state: SourceState) => { + if (state.imagePosition !== "embed") return; + const embeds = state.message?.embeds ?? []; + const nextIndex = embeds.findIndex((e) => e.image?.url); + if (nextIndex === -1) { + state.imagePosition = undefined; + state.imageEmbedIndex = undefined; + } else { + state.imageEmbedIndex = nextIndex; + } + }; @@ moveEmbedUp: (index) => set((state) => { if ( !state.message?.embeds || index <= 0 || index >= state.message.embeds.length ) return; const embeds = state.message.embeds; [embeds[index - 1], embeds[index]] = [ embeds[index], embeds[index - 1], ]; + syncImageEmbedIndex(state); }), @@ moveEmbedDown: (index) => set((state) => { if ( !state.message?.embeds || index < 0 || index >= state.message.embeds.length - 1 ) return; const embeds = state.message.embeds; [embeds[index], embeds[index + 1]] = [ embeds[index + 1], embeds[index], ]; + syncImageEmbedIndex(state); }), @@ deleteEmbed: (index) => set((state) => { if (!state.message?.embeds || index < 0 || index >= state.message.embeds.length) { return; } state.message.embeds.splice(index, 1); + syncImageEmbedIndex(state); }), @@ clearEmbeds: () => set((state) => { state.message = state.message ?? {}; state.message.embeds = []; + syncImageEmbedIndex(state); }),lib/dal.ts-215-222 (1)
215-222:prisma.source.deleterequires a unique filter.
guildIdis not unique in the Source model and there is no compound unique constraint on(guildId, id). The current code will fail type-checking. Sinceidis marked@id@unique``, delete byidalone is sufficient for uniqueness, or use `findFirst` to verify ownership before deletion.🛠️ Suggested fix
-export async function deleteSource(guildId: string, sourceId: number) { - return await prisma.source.delete({ - where: { - guildId: guildId, - id: sourceId, - }, - }); -} +export async function deleteSource(guildId: string, sourceId: number) { + const source = await prisma.source.findFirst({ + where: { guildId, id: sourceId }, + }); + if (!source) return null; + return prisma.source.delete({ where: { id: source.id } }); +}lib/dal.ts-290-304 (1)
290-304:addGuildToBetafails whenuserIdis omitted.The function is called without a
userIdargument in at least one place (lib/discord/guild.ts:175). WhenuserIdis undefined,connect: { id: "" }attempts to connect to a non-existent user with an empty ID, causing Prisma to throw. Since the schema definesuserIdas optional (String?), the user relation should only be set conditionally.✅ Suggested fix
export async function addGuildToBeta(guildId: string, userId?: string) { try { - return !!(await prisma.betaGuild.create({ - data: { - guild: { - connectOrCreate: { - where: {id: guildId}, - create: {id: guildId}, - }, - }, - user: { - connect: {id: userId || ""}, - }, - }, - })); + const data: Prisma.betaGuildCreateInput = { + guild: { + connectOrCreate: { + where: { id: guildId }, + create: { id: guildId }, + }, + }, + }; + if (userId) { + data.user = { connect: { id: userId } }; + } + return !!(await prisma.betaGuild.create({ data })); } catch (err) { console.error(err); return false; } }lib/actions.ts-163-178 (1)
163-178: Potential prototype pollution when assigning to embed.image.If
store.imageEmbedIndexcould be manipulated to be"__proto__"or similar, the assignmentembed.image = { url: "imageCard" }could polluteObject.prototype. WhileimageEmbedIndexis typed asnumber | undefined, ensure upstream validation guarantees it's a safe integer index.🛡️ Proposed fix to validate index
if ( store.imagePosition && store.imagePosition == "embed" && store.imageEmbedIndex !== undefined ) { + // Validate index is a safe integer + if (!Number.isInteger(store.imageEmbedIndex) || store.imageEmbedIndex < 0) { + return { + data: null, + done: false, + error: "Invalid embed index", + }; + } const embed = store.message.embeds?.[store.imageEmbedIndex];
🟡 Minor comments (25)
tsconfig.json-25-33 (1)
25-33: Consider re-addingforceConsistentCasingInFileNames: true.The include/exclude expansions look appropriate for the updated tooling. However, based on the summary,
forceConsistentCasingInFileNameswas removed from compilerOptions.This option is valuable for cross-platform development:
- Catches import casing mismatches (e.g.,
import from './MyFile'vs actualmyfile.ts)- Prevents issues where code works on Windows (case-insensitive) but fails on Linux (case-sensitive)
- Most CI/CD environments run on Linux, so this can cause unexpected deployment failures
Given the PR introduces "kebab-case conventions" changes, keeping this option enabled would help catch any casing inconsistencies early.
Suggested addition to compilerOptions
"strict": true, + "forceConsistentCasingInFileNames": true, "noEmit": true,eslint.config.mjs-6-12 (1)
6-12: Moveeslint-config-prettierto the end of the config array.The flat config specification states that later configurations take precedence when rules conflict. Currently,
prettieris first and will be overridden bynextVitalsandnextTs, allowing them to re-enable formatting rules that Prettier intends to disable. Placeprettierlast (after the Next configs) so it disables conflicting rules with final precedence.♻️ Suggested change
const eslintConfig = defineConfig([ - prettier, ...nextVitals, ...nextTs, + prettier, // Override default ignores of eslint-config-next. globalIgnores([components/layout/sidebar/sidebar-toggle.tsx-11-15 (1)
11-15: Addaria-labelfor accessibility.Icon-only buttons should have an accessible label for screen readers. Consider adding an
aria-labelthat reflects the current action.♿ Proposed fix for accessibility
<Button isIconOnly + aria-label={isOpen ? "Close sidebar" : "Open sidebar"} className="p-1.5 rounded-lg hidden sm:grid" onPress={() => setIsOpen(!isOpen)} >components/layout/sidebar/sidebar-item.tsx-19-49 (1)
19-49: Semantic HTML:<li>should be a direct child of<ul>, not wrapped by<Link>.The current structure has
<Link>wrapping<li>, which is invalid HTML. The<li>element must be a direct child of<ul>or<ol>. Consider inverting the structure.🔧 Proposed fix for semantic HTML
- <Link - href={link} - onClick={() => { - setIsOpen(false); - }} - > - <li - className={`relative flex items-center justify-center py-2 px-3 my-1 + <li + className={`relative flex items-center justify-center py-2 px-3 my-1 font-medium rounded-md cursor-pointer transition-colors group hover:text-indigo-800 ${active ? "bg-gradient-to-tr from-indigo-200 to-indigo-100 text-indigo-800" : "hover:bg-indigo-50 text-white-600"}`} - > - {icon} + > + <Link + href={link} + className="flex items-center w-full" + onClick={() => { + setIsOpen(false); + }} + > + {icon} <span className={`overflow-hidden transition-all sm:block hidden ${ isOpen ? "w-48 ml-3" : "w-0" }`} > {text} </span> - - {!isOpen && ( - <div - className={`hidden sm:absolute left-full rounded-md px-2 py-1 ml-6 bg-indigo-100 text-indigo-800 text-sm invisible opacity-20 -translate-x-3 transition-all group-hover:visible group-hover:opacity-100 group-hover:translate-x-0`} - > - {text} - </div> - )} - </li> - </Link> + </Link> + {!isOpen && ( + <div + className={`hidden sm:absolute left-full rounded-md px-2 py-1 ml-6 bg-indigo-100 text-indigo-800 text-sm invisible opacity-0 -translate-x-3 transition-all group-hover:visible group-hover:opacity-100 group-hover:translate-x-0`} + > + {text} + </div> + )} + </li>components/layout/sidebar/sidebar.tsx-72-77 (1)
72-77: Remove "Home" item or restructure to match the route hierarchy.The "Home" item will never show as active because
useSelectedLayoutSegment()at/dashboard/[guildId]returnsnull(not"home"), which falls back to"dashboard". Moreover, clicking "Home" navigates to/dashboard, which is outside the[guildId]layout entirely, causing the sidebar to unmount. The navigation structure should either remove this item or redirect it to/dashboard/[guildId]with appropriate active state logic.components/dashboard/guild/editor/embed/fields/embed-field-inline-input.tsx-17-27 (1)
17-27: GuardeditFieldwhen the embed is missing.
Ifembedis undefined, the checkbox still renders andonValueChangecan calleditFieldwith a stale index.Suggested fix
const embed = useStore(store, (state) => state.message?.embeds?.[embedIndex]); const editField = useStore(store, (state) => state.editField); - const fieldInline = embed?.fields?.[fieldIndex]?.inline; + if (!embed) return null; + const fieldInline = embed.fields?.[fieldIndex]?.inline;lib/prisma.ts-5-7 (1)
5-7: Missing validation forDATABASE_URLcan produce cryptic errors.If
DATABASE_URLis not set,connectionStringbecomes the literal string"undefined", which will cause a confusing database connection error. Consider validating presence early.🛡️ Proposed fix to validate environment variable
-const connectionString = `${process.env.DATABASE_URL}`; +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + throw new Error("DATABASE_URL environment variable is not set"); +} const adapter = new PrismaPg({ connectionString });app/(home)/status/page.tsx-31-37 (1)
31-37: Timer countdown can go negative.The decrement interval runs independently and will continue past zero, showing "Updating in -1 seconds", "-2 seconds", etc. if the status fetch takes longer than expected or fails.
🐛 Proposed fix to clamp at zero
useEffect(() => { const interval = setInterval(() => { - setUpdateTime((prev) => prev - 1); + setUpdateTime((prev) => Math.max(0, prev - 1)); }, 1000); // Update every second return () => clearInterval(interval); }, []);components/dashboard/guild/editor/embed/author/embed-author-icon.tsx-16-16 (1)
16-16: Typo:editEmbebshould beeditEmbed.📝 Proposed fix
- const editEmbeb = useStore(store, (state) => state.editEmbed); + const editEmbed = useStore(store, (state) => state.editEmbed);Also update the usage on line 26:
- editEmbeb(embedIndex, { + editEmbed(embedIndex, {app/dashboard/[guildId]/leave/image/layout.tsx-39-39 (1)
39-39: Type assertion may mask data schema mismatches.The
as BaseCardConfigassertion trusts that the stored JSON inimageCard.datamatches theBaseCardConfiginterface. If the schema evolves or data is corrupted, this could cause runtime errors. Consider adding validation (e.g., using Zod) or a type guard.components/dashboard/guild/enable-module-button.tsx-36-38 (1)
36-38: Useconsole.errorfor error logging.Error conditions should be logged with
console.errorrather thanconsole.logfor proper log level semantics and better debugging experience.🐛 Suggested fix
} catch (e) { - console.log("error", e); + console.error("error", e); if (e instanceof Error) {components/dashboard/guild/editor/image-position-editor.tsx-35-36 (1)
35-36: Remove debug logging before shipping.The
console.logwill leak into production and adds noise.🧹 Suggested fix
- console.log("hasImage", hasImage);lib/discord/text.tsx-268-312 (1)
268-312: Mention highlight should be based on raw content.
contentis often a React node array, socontent?.includes("{user}")returns false even when a mention is present. Use the original message text for the highlight check.✅ Suggested fix
- const content = message.content + const rawContent = message.content ?? ""; + const content = rawContent ? parseMessageText(message.content, user, guild) : null; ... - highlight={content?.includes("{user}")} + highlight={rawContent.includes("{user}")}state/source.ts-51-60 (1)
51-60: Default message content looks like a leftover test string.If this state can surface in UI, “test” will be user-visible. Consider using an empty string or a meaningful default.
🛠️ Suggested fix
message: { - content: "test", + content: "", embeds: [defaultEmbed], },components/dashboard/guild/editor/send-menu.tsx-123-140 (1)
123-140: Avoid<ul>inside<p>in tooltip content.This is invalid HTML and can confuse screen readers. Use a
<div>wrapper or separate the list from paragraph text.🛠️ Suggested fix (pattern)
- <p> - You need to grant Welcomer Beta the - following permissions for this channel: - <ul className="list-disc pl-5"> + <div> + <div> + You need to grant Welcomer Beta the + following permissions for this channel: + </div> + <ul className="list-disc pl-5"> ... - </ul> - </p> + </ul> + </div>Also applies to: 199-215
CHANGES.md-1-82 (1)
1-82: Polish French typography/punctuation in the changelog.LanguageTool flags multiple minor issues (e.g., “Preview” → “Prévisualisation”, incorrect apostrophes, and missing punctuation). A quick pass will improve readability.
✍️ Example fix (header)
-# Modifications - Preview d'image et positionnement +# Modifications – Prévisualisation d'image et positionnementcomponents/dashboard/guild/editor/send-menu.tsx-86-162 (1)
86-162: Add a key to the fragment or returnSelectSectiondirectly.The outer element returned by the map is a fragment without a key. React requires the outermost element in a mapped list to be keyed. Either add
key={channel.id}to the fragment or remove the fragment wrapper entirely and returnSelectSectiondirectly with its existing key.🛠️ Suggested fix
- return ( - <> - <SelectSection - key={channel.id} - showDivider - title={channel.name} - > + return ( + <SelectSection + key={channel.id} + showDivider + title={channel.name} + >lib/actions.ts-699-699 (1)
699-699: Unhandled result from deleteImageCardInternal could mask errors.The result of
deleteImageCardInternalis not checked. If deletion fails, the function will still return success, potentially leaving the system in an inconsistent state.🐛 Proposed fix
- await deleteImageCardInternal(source.activeCardId, guildId); + const deleteResult = await deleteImageCardInternal(source.activeCardId, guildId); + if (!deleteResult.done) { + return deleteResult; + } return { done: true, error: null, };app/dashboard/[guildId]/page.tsx-47-48 (1)
47-48: Unsafe type casting of searchParams to Period enum.Casting user-controlled input directly to
Periodwithout validation allows invalid values to pass through. If an invalid period string is passed, it could cause unexpected behavior downstream.🛡️ Proposed fix with validation
+const isValidPeriod = (value: string | undefined): value is Period => { + return value !== undefined && Object.values(Period).includes(value as Period); +}; + -const welcomerPeriod = (search.welcomerPeriod as Period) || Period.DAILY; -const leaverPeriod = (search.leaverPeriod as Period) || Period.DAILY; +const welcomerPeriod = isValidPeriod(search.welcomerPeriod) + ? search.welcomerPeriod + : Period.DAILY; +const leaverPeriod = isValidPeriod(search.leaverPeriod) + ? search.leaverPeriod + : Period.DAILY;components/dashboard/guild/remove-module-button.tsx-35-43 (1)
35-43: Missing error handling for removeSource.If
removeSourcethrows an error, the user receives no feedback andreset()is never called. Consider wrapping in try/catch and showing an error toast.🛡️ Proposed fix
+import { toast } from "react-toastify"; + async function handleSubmit() { plausible("RemoveModuleButton", { props: { sourceType, }, }); - await removeSource(guildId, sourceId); - reset(); + try { + await removeSource(guildId, sourceId); + reset(); + } catch (error) { + toast.error(error instanceof Error ? error.message : "Failed to disable module"); + } }components/dashboard/guild/image-editor/components/save-button.tsx-31-33 (1)
31-33: Avoid mutating refs during render.Setting
lastSavedDataRef.currentduring the render phase is problematic in React Strict Mode (which runs effects/renders twice in development). This could lead to unexpected behavior.♻️ Proposed fix using useEffect
+import { useContext, useEffect, useRef, useState } from "react"; -import { useContext, useRef, useState } from "react"; // Track the last saved state to compare against const lastSavedDataRef = useRef<string | null>(null); -// Initialize the last saved data ref on first render -if (lastSavedDataRef.current === null) { - lastSavedDataRef.current = JSON.stringify(store.getInitialState().data); -} +// Initialize the last saved data ref +useEffect(() => { + if (lastSavedDataRef.current === null) { + lastSavedDataRef.current = JSON.stringify(store.getInitialState().data); + } +}, [store]);components/dashboard/guild/stats/stats-viewer.tsx-19-19 (1)
19-19: Fragile string manipulation forcountableModule.The logic
module.slice(0, module.length - 1) + "d"produces incorrect results for some module names. For example:
- "Welcomer" → "Welcomed" ✓
- "Leaver" → "Leaverd" ✗ (should be "Left" or "Members left")
Consider using an explicit mapping for grammatically correct labels.
💡 Proposed fix with explicit mapping
+const countableModuleMap: Record<string, string> = { + Welcomer: "welcomed", + Leaver: "left", +}; + export default async function StatsViewer({ ... }) { const data = await fetchGuildStat(guildId, period, module); const formattedModule = `${module[0].toUpperCase() + module.slice(1)} stats`; - const countableModule = module.slice(0, module.length - 1) + "d"; + const countableModule = countableModuleMap[module] ?? module.toLowerCase();components/dashboard/guild/image-editor/components/preview.tsx-25-33 (1)
25-33: Fix loading skeleton conditional (??prevents rendering).
isLoading ?? (...)uses the nullish coalescing operator, which only evaluates the right side when the left operand isnullorundefined. SinceisLoadingis a boolean, it's never null, so the skeleton never renders. UseisLoading && (...)to conditionally render the loader when true.🛠️ Proposed fix
- { - isLoading ?? ( - <Skeleton> - <div className="flex justify-center items-center" style={{width: "800px", height: "300px"}}> - Loading - </div> - </Skeleton> - ) - } + {isLoading && ( + <Skeleton> + <div className="flex justify-center items-center" style={{width: "800px", height: "300px"}}> + Loading + </div> + </Skeleton> + )}components/dashboard/guild/editor/embed/author/embed-author-url.tsx-9-29 (1)
9-29: Prevent rendering the Input when embed is undefined to avoid silent failures on user input.If the embed at
embedIndexdoesn't exist, the component still renders an editable Input field, but any changes will silently fail becauseeditEmbedguards against missing embeds. Wrap the return in a guard or early return to prevent this confusing UX.components/dashboard/guild/image-editor/components/background-editor.tsx-33-34 (1)
33-34: Remove debug console.log statements before merging.These debug statements should not be present in production code.
🧹 Proposed fix
// Check if backgroundImgURL is not null (including empty string) const hasImage = backgroundImgURL !== null; - console.log("hasImage", hasImage); - console.log("backgroundImgURL", backgroundImgURL);
| import {parseMessageToReactElement} from "@/lib/discord/text"; | ||
| import {UserObject} from "@/lib/discord/user"; | ||
| import {SourceState} from "@/state/source"; | ||
| import {useMemo, useState} from "react"; |
There was a problem hiding this comment.
Add useEffect to imports.
The fix for the DOM access issue requires useEffect, which is not currently imported.
🔧 Update import
-import {useMemo, useState} from "react";
+import {useEffect, useMemo, useState} from "react";📝 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.
| import {useMemo, useState} from "react"; | |
| import {useEffect, useMemo, useState} from "react"; |
🤖 Prompt for AI Agents
In `@components/dashboard/guild/editor/message-preview.tsx` at line 7, The import
line in message-preview.tsx currently imports useMemo and useState from React
but is missing useEffect; update the import statement (the line containing
"import {useMemo, useState} from \"react\";") to also include useEffect so the
MessagePreview component (and any DOM access logic using useEffect) can use it.
| const htmlCanvasElement = document.getElementById("preview-canvas"); | ||
| if (htmlCanvasElement instanceof HTMLCanvasElement) { | ||
| setCanvasElement(htmlCanvasElement); | ||
| } |
There was a problem hiding this comment.
Critical: DOM access during render causes infinite re-renders.
Calling document.getElementById and setCanvasElement directly in the render body creates an infinite loop: each setCanvasElement call triggers a re-render, which runs this code again, calling setCanvasElement again, and so on.
Additionally, document may not be available during SSR hydration, potentially causing runtime errors.
🐛 Proposed fix using useEffect
- const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | undefined>(undefined);
-
- const htmlCanvasElement = document.getElementById("preview-canvas");
- if (htmlCanvasElement instanceof HTMLCanvasElement) {
- setCanvasElement(htmlCanvasElement);
- }
+ const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | undefined>(undefined);
+
+ useEffect(() => {
+ const htmlCanvasElement = document.getElementById("preview-canvas");
+ if (htmlCanvasElement instanceof HTMLCanvasElement) {
+ setCanvasElement(htmlCanvasElement);
+ }
+ }, []);📝 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 htmlCanvasElement = document.getElementById("preview-canvas"); | |
| if (htmlCanvasElement instanceof HTMLCanvasElement) { | |
| setCanvasElement(htmlCanvasElement); | |
| } | |
| const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | undefined>(undefined); | |
| useEffect(() => { | |
| const htmlCanvasElement = document.getElementById("preview-canvas"); | |
| if (htmlCanvasElement instanceof HTMLCanvasElement) { | |
| setCanvasElement(htmlCanvasElement); | |
| } | |
| }, []); |
🤖 Prompt for AI Agents
In `@components/dashboard/guild/editor/message-preview.tsx` around lines 25 - 28,
The render body in message-preview.tsx currently calls
document.getElementById("preview-canvas") and setCanvasElement directly, causing
infinite re-renders and SSR issues; move that DOM access into a useEffect (or
useLayoutEffect) that runs on mount and/or when dependencies change, or better
yet attach a React ref to the canvas and call setCanvasElement(ref.current)
inside the effect; ensure you remove direct document access from the component
body, guard any document use with typeof document !== "undefined" for SSR, and
update references to preview-canvas, htmlCanvasElement, and setCanvasElement
accordingly.
| export default nextConfig; | ||
|
|
||
| // Injected content via Sentry wizard below | ||
|
|
||
| export default withSentryConfig(nextConfig, { | ||
| async headers() { | ||
| return [ | ||
| { | ||
| source: "/:path*", | ||
| headers: [ | ||
| { | ||
| key: "Document-Policy", | ||
| value: "js-profiling", | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| }, | ||
| module.exports = withSentryConfig(module.exports, { |
There was a problem hiding this comment.
Mixing ES module export with CommonJS module.exports will break the configuration.
Line 20 uses export default nextConfig (ES modules), but line 24 uses module.exports = withSentryConfig(module.exports, {...}) (CommonJS). At line 24, module.exports won't contain nextConfig since it was exported via ES module syntax, resulting in an empty or undefined configuration being passed to Sentry.
The correct approach is to use ES modules consistently:
🐛 Proposed fix
-export default nextConfig;
-
-// Injected content via Sentry wizard below
-
-module.exports = withSentryConfig(module.exports, {
+export default withSentryConfig(nextConfig, {📝 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.
| export default nextConfig; | |
| // Injected content via Sentry wizard below | |
| export default withSentryConfig(nextConfig, { | |
| async headers() { | |
| return [ | |
| { | |
| source: "/:path*", | |
| headers: [ | |
| { | |
| key: "Document-Policy", | |
| value: "js-profiling", | |
| }, | |
| ], | |
| }, | |
| ]; | |
| }, | |
| module.exports = withSentryConfig(module.exports, { | |
| export default withSentryConfig(nextConfig, { |
🤖 Prompt for AI Agents
In `@next.config.ts` around lines 20 - 24, You are mixing ES module export (export
default nextConfig) with CommonJS (module.exports = ...); replace the CommonJS
line so the Sentry wrapper is applied to the ES export instead: call
withSentryConfig with nextConfig (not module.exports) and export the wrapped
config as the default export (i.e., export default withSentryConfig(nextConfig,
{...})), and remove the module.exports usage; ensure withSentryConfig is
imported/available before use.
| datasource db { | ||
| provider = "postgresql" | ||
| url = env("DATABASE_URL") | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Prisma schema datasource url required field
💡 Result:
In a Prisma schema (schema.prisma), the datasource block requires both:
provider(required)url(required)
Prisma’s schema reference explicitly marks url as required (it’s the connection string, often set via env("DATABASE_URL")). [1]
Example:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}If you’re using a pooled URL (e.g., Prisma Accelerate/pgBouncer), you still keep url, and you can optionally add directUrl for CLI/migrations that need a direct connection. [1]
🌐 Web query:
Prisma missing url field error prisma validate prisma generate
💡 Result:
This error happens when Prisma can’t resolve a datasource connection URL at validate/generate time—either the datasource block is missing url, or url = env("...") points to an unset/empty env var.
Fix checklist
-
Ensure your
schema.prismahas a datasourceurldatasource db { provider = "postgresql" // or mysql/sqlserver/sqlite/mongodb/etc. url = env("DATABASE_URL") }
Prisma’s schema reference lists
urlas a required datasource field. (prisma.io) -
Ensure
DATABASE_URLis actually set when you run Prisma CLI- Put it in a
.envin the project root (commonly wherepackage.jsonis), e.g.DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DBNAME"
- Or export it in your shell/CI environment.
If Prisma can’t find it, you’ll get “Missing required environment variable: DATABASE_URL”. (github.com)
If it’s set but empty, Prisma will complain the datasource URL is empty/nonexistent. (github.com)
- Put it in a
-
Run from the project root and re-run generate
npx prisma validate npx prisma generate
Note if you recently upgraded Prisma
There are active Prisma v7-related reports where DATABASE_URL is required for prisma generate (e.g., in CI) and/or behavior around datasource URLs/config changed. If you upgraded to v7 and this started happening, check your version and review those issues. (github.com)
If you paste your exact error text plus your datasource db { ... } block (and say your Prisma version), I can pinpoint which of the above is triggering it.
Citations:
- 1: https://www.prisma.io/docs/orm/reference/prisma-schema-reference?utm_source=openai
- 2: https://github.com/orgs/community/discussions/178993?utm_source=openai
- 3: Prisma generate requires nonempty URL environment for the datasource prisma/prisma#2321
- 4: DATABASE_URL is required for prisma generate in v7 prisma/prisma#28590
Add the required datasource url field.
Prisma datasource requires both provider and url. Omitting url will cause prisma validate and prisma generate to fail.
🛠️ Suggested fix
datasource db {
provider = "postgresql"
+ url = env("DATABASE_URL")
}📝 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.
| datasource db { | |
| provider = "postgresql" | |
| url = env("DATABASE_URL") | |
| } | |
| datasource db { | |
| provider = "postgresql" | |
| url = env("DATABASE_URL") | |
| } |
🤖 Prompt for AI Agents
In `@prisma/schema.prisma` around lines 6 - 8, The Prisma datasource block named
`db` is missing the required `url` field; update the `datasource db` definition
to include a `url` attribute (for example using an environment variable like
env("DATABASE_URL") or a full connection string) so Prisma commands such as
`prisma validate` and `prisma generate` succeed; modify the `datasource db`
block in `schema.prisma` to add the `url` entry alongside the existing
`provider` entry.
| import { decrypt, getSession } from "@/lib/session"; | ||
|
|
||
| export default async function middleware(req: NextRequest) { | ||
| export default async function proxy(req: NextRequest) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if middleware.ts exists and properly imports from proxy.ts
# Find middleware files in common locations
echo "=== Searching for middleware files ==="
fd -t f "middleware\.(ts|js)$"
# Check if any middleware file imports from proxy
echo -e "\n=== Checking for imports from proxy ==="
rg -l "from.*proxy" --type ts --type js
# If middleware.ts exists, show its content
echo -e "\n=== Contents of middleware files (if any) ==="
fd -t f "middleware\.(ts|js)$" --exec cat {}
# Also check for proxy.ts file
echo -e "\n=== Checking proxy.ts file ==="
cat proxy.ts
# List all files in the repository root
echo -e "\n=== Files in repository root ==="
ls -la | grep -E "\.(ts|js)$"Repository: Welcomer-Bot/welcomer-client
Length of output: 1457
File must be named middleware.ts, not proxy.ts, for Next.js to recognize and execute the middleware.
Next.js only runs middleware from files named middleware.ts (or middleware.js) at the repository root or within src/ or app/ directories. The config export with matcher pattern is only recognized in files with this specific name. Since this file is named proxy.ts, the middleware logic will not execute regardless of the function name.
Rename the file from proxy.ts to middleware.ts to restore functionality.
🤖 Prompt for AI Agents
In `@proxy.ts` at line 5, The middleware won't run because the file is named
proxy.ts; rename the file to middleware.ts so Next.js recognizes and executes
the exported handler (export default async function proxy). After renaming,
verify the existing export default async function proxy and any exported
config/matcher remain in the file (no other changes needed) so Next.js can pick
up the middleware.
There was a problem hiding this comment.
Actionable comments posted: 7
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/dashboard/guild/editor/embed/fields/embed-field-value-input.tsx (1)
26-30: Incorrect label and validation message – refers to "Footer" instead of "Field".The label displays "Footer text" and the validation error mentions "Footer", but this component is for embed field values. This appears to be a copy-paste error.
📝 Proposed fix
<Input type="text" - label={`Footer text ( ${fieldValue?.length ?? 0}/1024 )`} - aria-label="Text" + label={`Field value ( ${fieldValue?.length ?? 0}/1024 )`} + aria-label="Field value" validate={(value) => { if (value.length > 1024) - return "Footer must not exceed 1024 characters!"; + return "Field value must not exceed 1024 characters!"; }}components/dashboard/guild/editor/embed/fields/embed-field-name-input.tsx (1)
26-29: Incorrect label and validation message – refers to "Footer" instead of "Field name".Same issue as in
embed-field-value-input.tsx: the label displays "Footer text" and validation mentions "Footer", but this component is for embed field names.📝 Proposed fix
<Input type="text" - label={`Footer text ( ${fieldName?.length ?? 0}/256 )`} - aria-label="Text" + label={`Field name ( ${fieldName?.length ?? 0}/256 )`} + aria-label="Field name" validate={(value) => { - if (value.length > 256) return "Footer must not exceed 256 characters!"; + if (value.length > 256) return "Field name must not exceed 256 characters!"; }}
🤖 Fix all issues with AI agents
In `@components/dashboard/guild/editor/embed/body/embed-body-color-input.tsx`:
- Around line 18-31: The `@heroui` Input does not support type="color", so replace
the Input usage with a native <input type="color"> (or a documented color
picker) and wire its value/onChange to the existing editEmbed logic: use
embedColor ? `#${embedColor.toString(16).padStart(6,'0')}` for the input value,
onChange should call editEmbed(embedIndex, {...embed, color: e.target.value ?
parseInt(e.target.value.replace("#",""), 16) : undefined}); remove the
unsupported isClearable prop and, if you need clear behavior, add a small clear
button that calls editEmbed(..., { color: undefined }) alongside the color
input. Ensure you reference the same identifiers (editEmbed, embedIndex, embed,
embedColor) when making the changes.
In `@components/dashboard/guild/image-editor/hooks/use-image-editor.ts`:
- Around line 19-30: The hook's image generation path mixes await with .then()
and fails to clear loading on errors: when calling generateImage (function
generateImage in use-image-editor hook) ensure you await it without chaining
.then(), call setIsLoading(false) in a finally block (or after try/catch) so
both success and error paths reset loading, and keep the console.log after the
awaited call; also keep the catch setting setError(error instanceof Error ?
error.message : "Failed to generate image") as is.
In `@components/dashboard/guild/manage-button.tsx`:
- Line 14: The URL-building line using module.slice(0, -1).toLowerCase()
incorrectly transforms the SourceType "DM" into "d"; replace this fragile
transformation with an explicit mapping from SourceType values to URL segments
(e.g., a map object keyed by module or SourceType) and use that map when
computing link (the variable named link in manage-button.tsx) so "DM" resolves
to "dm" while other types (e.g., "Welcomer", "Leaver") map to their expected
segments; update any references that compute link from module to use this
mapping.
In `@components/shared/status/guild-input.tsx`:
- Around line 42-46: The conditional rendering uses shardId !== undefined but
shardId is typed number | null, so change the check to exclude null (e.g.,
shardId !== null or Number.isFinite(shardId)) so the "Selected Shard ID" block
only renders when shardId is an actual number; update the JSX conditional around
shardId and any related display to avoid rendering "null" (refer to the shardId
variable in components/shared/status/guild-input.tsx and the surrounding JSX).
In `@lib/dal.ts`:
- Around line 165-169: createModuleStats uses Object.values(Period).forEach with
an async callback so createGuildStats calls are not awaited; change
createModuleStats to await all calls by mapping Period values to promises and
using Promise.all (or use a for...of loop with await) so each
createGuildStats(guildId, period, source) completes before returning; reference
the createModuleStats function and createGuildStats invocation and ensure
SourceType/Period usage remains unchanged.
In `@lib/discord/image.ts`:
- Around line 1-4: This client component imports and uses React's server-only
cache() which breaks at runtime; remove the cache import and any cache(...)
wrapper around functions/components in lib/discord/image.ts and instead use
client-side memoization (useMemo/useCallback) or a client caching library (React
Query/SWR) for the same behavior; also remove the debug console.log statement
present in the file. Ensure you delete the "import {cache} from 'react';" line
and replace wrapped functions with appropriate useMemo/useCallback usage where
needed (or leave them plain if not necessary) and remove the stray console.log
call.
In `@lib/dto.ts`:
- Line 3: Update the import in lib/dto.ts to match the Prisma generator output:
change the module import for the types GuildStats, Period, and SourceType from
"../generated/prisma/client" to "../generated/prisma" so it references the
configured output directory and resolves the generated types correctly.
🟠 Major comments (19)
utils/formatter.ts-13-24 (1)
13-24: Missingauthorandfieldsproperties will cause data loss.The
APIEmbedtype includesauthorandfieldswhich are commonly used embed properties. The current implementation silently drops these when formatting, which could break embed rendering.🐛 Proposed fix to include missing embed fields
export const formatDiscordEmbed = (embed: APIEmbed):APIEmbed => { return { title: embed.title ?? undefined, description: embed.description ?? undefined, url: embed.url ?? undefined, timestamp: embed.timestamp ?? undefined, color: embed.color ?? undefined, footer: embed.footer ?? undefined, image: embed.image ?? undefined, thumbnail: embed.thumbnail ?? undefined, + author: embed.author ?? undefined, + fields: embed.fields?.length ? embed.fields : undefined, } }lib/session.ts-6-6 (1)
6-6: Update remaining Prisma client imports to be consistent across the codebase.The import path in lib/session.ts is correct per the Prisma schema output configuration. However, other files (
state/source.ts,lib/actions.ts) still use the old@/generated/prisma/clientalias. To maintain consistency after the Prisma upgrade, update all imports to use the relative path../generated/prisma/clientmatching the pattern now used inlib/dto.ts,lib/prisma.ts, andlib/dal.ts.styles/globals.css-1-6 (1)
1-6: EnabletailwindDirectivesin Biome's CSS parser configuration.The
biome.jsonfile lacks thetailwindDirectivessetting in the CSS parser configuration. Since the CSS linter is enabled and this file contains Tailwind directives (@import,@plugin,@source,@custom-variant), Biome will report these as invalid CSS without explicit support enabled.Add the following to
biome.json:"css": { "linter": { "enabled": true }, "parser": { "cssModules": true, "tailwindDirectives": true } }lib/dal.ts-393-399 (1)
393-399: Redundant duplicate call togetGuild.
getGuildis called twice with effectively the same ID (guildIdand thendata.id). The second call is unnecessary sincedatais already the guild.🐛 Proposed fix
export async function getGuildData(guildId: string) { - const data = await getGuild(guildId); - if (!data) return null; - const guild = await getGuild(data.id); + const guild = await getGuild(guildId); if (!guild) return null; return guild.toObject(); }components/ui/buttons/request-beta-access-button.tsx-7-7 (1)
7-7:redirect()in an event handler will throw; useuseRouter().push()instead.
redirectis only supported during Client Component render; event handlers (onPress,onClick, etc.) requireuseRouter().push()oruseRouter().replace(). Line 77 callsredirect()insideonPress, which will fail at runtime.🛠️ Proposed fix
-import {redirect} from "next/navigation"; +import {useRouter} from "next/navigation"; ... export default function RequestBetaAccessButton() { const plausible = usePlausible(); + const router = useRouter(); const {isOpen, onOpen, onOpenChange} = useDisclosure(); ... <Button color="primary" onPress={() => { plausible("request-beta-access-join-server"); onClose(); - redirect("/support"); + router.push("/support"); }} >sentry.edge.config.ts-14-19 (1)
14-19: GatesendDefaultPiiandenableLogsby environment in edge contexts.In Next.js Edge runtime (
sentry.edge.config.ts), always-onsendDefaultPii: truecan send cookies, user identity, and IP addresses to Sentry. Sentry's official guidance recommends keepingsendDefaultPiidisabled in edge configs unless explicitly needed for debugging. Similarly,enableLogs: trueshould be controlled to avoid unnecessary log transmission in production edge functions.🛠️ Suggested hardening
Sentry.init({ dsn: "https://1e4ac005f063c4e051f529596491a4b8@o4508545743388672.ingest.de.sentry.io/4508875963826256", // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. tracesSampleRate: 1, // Enable logs to be sent to Sentry - enableLogs: true, + enableLogs: process.env.NODE_ENV === "production", // Enable sending user PII (Personally Identifiable Information) // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii - sendDefaultPii: true, + sendDefaultPii: process.env.NEXT_PUBLIC_SENTRY_PII === "true", });lib/prisma.ts-5-10 (1)
5-10: Guard against missingDATABASE_URL(currently becomes"undefined").
Line 5 uses a template literal, which convertsundefinedinto the string"undefined". This hides missing env configuration and can lead to hard‑to‑diagnose connection errors at runtime.🛠️ Proposed fix
-const connectionString = `${process.env.DATABASE_URL}`; +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + throw new Error("DATABASE_URL is not set"); +}sentry.server.config.ts-7-18 (1)
7-18: Compliance/privacy risk:sendDefaultPii: trueand removed production gating.Two concerns with this configuration:
PII transmission enabled:
sendDefaultPii: truesends user IP addresses, cookies, and other personally identifiable information to Sentry. This may conflict with GDPR/CCPA requirements. Ensure this is intentional and that your privacy policy covers this data collection.Environment gating removed: The previous
enabled: process.env.NODE_ENV === "production"check was removed. Sentry will now initialize in all environments, which can:
- Pollute production error data with development noise
- Consume your Sentry quota unnecessarily
- Inadvertently send sensitive development data
Consider re-adding environment gating:
Proposed fix to restore environment gating
Sentry.init({ dsn: "https://1e4ac005f063c4e051f529596491a4b8@o4508545743388672.ingest.de.sentry.io/4508875963826256", + + // Only enable Sentry in production + enabled: process.env.NODE_ENV === "production", // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. tracesSampleRate: 1,components/dashboard/guild/editor/embed/footer/embed-footer-text-input.tsx-11-33 (1)
11-33: Guard against missing embed to prevent runtime crashes.
...embed!assumes the embed exists; when it doesn’t,onValueChangethrows. Add an early return or disable the input until the embed is present.🛡️ Suggested fix
const embed = useStore(store, (state) => state.message?.embeds?.[embedIndex]); const editEmbed = useStore(store, (state) => state.editEmbed); + if (!embed) return null; return ( <Input @@ onValueChange={(value) => editEmbed(embedIndex, { - ...embed!, + ...embed, footer: { ...embed?.footer, text: value || "", - icon_url: embed?.footer?.icon_url || "", // Ensure 'icon_url' is always defined + icon_url: embed?.footer?.icon_url || undefined, }, }) }components/dashboard/guild/editor/embed/footer/embed-footer-icon-input.tsx-11-29 (1)
11-29: Guard against missing embed to prevent runtime crashes.
...embed!will throw if the embed isn’t loaded or the index is invalid. Add a guard (or disable the input) before callingeditEmbed.🛡️ Suggested fix
const embed = useStore(store, (state) => state.message?.embeds?.[embedIndex]); const editEmbed = useStore(store, (state) => state.editEmbed); + if (!embed) return null; return ( <Input type="url" @@ onValueChange={(value) => editEmbed(embedIndex, { - ...embed!, + ...embed, footer: { ...embed?.footer, icon_url: value || undefined, text: embed?.footer?.text || "", // Ensure 'text' is always defined }, }) }components/dashboard/guild/editor/embed/footer/embed-footer-text-input.tsx-28-31 (1)
28-31: Remove empty string coercion foricon_url.Empty strings are invalid according to Discord API and cause validation errors: "Not a well formed URL." The
icon_urlfield should be omitted entirely when no valid URL is available. Preserveundefinedor conditionally set the property only when a valid URL exists.components/shared/status/guild-input.tsx-14-28 (1)
14-28: Add debouncing to avoid excessive API calls on every keystroke.Currently, every character typed triggers
fetchGuildShardId. This can overload the API and degrade UX. Additionally, ifonShardSelectedis not memoized by the parent, it will cause the effect to re-run on every render.♻️ Suggested improvement with debounce
+ import { useMemo } from "react"; + export const GuildInput = ({ onShardSelected }: { ... }) => { const [guildId, setGuildId] = useState(""); const [shardId, setShardId] = useState<number | null>(null); + const [debouncedGuildId, setDebouncedGuildId] = useState(""); + + // Debounce the guild ID input + useEffect(() => { + const timer = setTimeout(() => setDebouncedGuildId(guildId), 500); + return () => clearTimeout(timer); + }, [guildId]); useEffect(() => { const fetchGuildShard = async (guildId: string) => { // ... }; - if (guildId) { - fetchGuildShard(guildId); + if (debouncedGuildId) { + fetchGuildShard(debouncedGuildId); } - }, [guildId, onShardSelected]); + }, [debouncedGuildId, onShardSelected]);Also consider documenting that
onShardSelectedshould be memoized (e.g., wrapped inuseCallback) by the parent to prevent unnecessary effect re-runs.lib/discord/text.tsx-310-310 (1)
310-310: Bug:highlightprop check will not work correctly.
contentis aReactNode[]returned byparseMessageText, not a string. Callingcontent?.includes("{user}")on an array of React nodes will never match the string"{user}"since the{user}pattern has already been replaced withDiscordMentioncomponents by this point.The highlight should always be true when the original message contained
{user}, or this check should be performed on the rawmessage.contentstring before parsing.🐛 Proposed fix
+ // Check for user mention in original content before parsing + const shouldHighlight = message.content?.includes("{user}") ?? false; + return ( <DiscordMessages className="h-full"> <DiscordMessage author={"Welcomer"} avatar="/logo.svg" bot verified - highlight={content?.includes("{user}")} + highlight={shouldHighlight} className="pl-4" >package.json-16-16 (1)
16-16: Replace@discordjs/buildersdev build with stable version.The version
2.0.0-dev.1755864085-55ab46dbcis a deprecated development build with no stable 2.0.0 release available yet. Upgrade to the latest stable version1.11.3(released 2025-08-10):"@discordjs/builders": "^1.11.3"Using deprecated builds risks instability and compatibility issues in production.
package.json-24-25 (1)
24-25: Sentry package version mismatch—profiling will not function.
@sentry/profiling-nodemust use the exact same version as@sentry/nextjs(major.minor.patch). With@sentry/nextjsat^10, update@sentry/profiling-nodeto match the exact version of@sentry/nextjs(e.g., both at10.36.0).components/dashboard/guild/remove-module-button.tsx-35-43 (1)
35-43: Add error handling to matchEnableModuleButtonpattern.
handleSubmitlacks try/catch error handling. IfremoveSourcethrows, the user receives no feedback andreset()is never called. Compare withEnableModuleButtonwhich properly handles errors with toast notifications.🛠️ Suggested fix
+import { toast } from "react-toastify"; + // ... existing imports async function handleSubmit() { plausible("RemoveModuleButton", { props: { sourceType, }, }); - await removeSource(guildId, sourceId); - reset(); + try { + await removeSource(guildId, sourceId); + reset(); + } catch (e) { + console.log("error", e); + if (e instanceof Error) { + toast.error(e.message || "An error occurred"); + } else { + toast.error("An error occurred"); + } + } }app/dashboard/[guildId]/leave/image/layout.tsx-25-29 (1)
25-29: Scope imageCard lookup to the current source.The
findUniquebyidalone could surface an ImageCard from a different source if theactiveCardIdvalue becomes stale or corrupted. Since ImageCard has asourceIdfield that must match the current source, add a scope check to ensure the fetched card belongs to the expected source.🔒 Suggested fix
- if (source?.activeCardId) { - imageCard = await prisma.imageCard.findUnique({ - where: { id: source.activeCardId }, - }); - } + if (source?.activeCardId) { + imageCard = await prisma.imageCard.findFirst({ + where: { + id: source.activeCardId, + sourceId: source.id, + }, + }); + }app/dashboard/[guildId]/welcome/image/layout.tsx-24-28 (1)
24-28: Scope imageCard lookup to the current source.
findUniquebyidalone lacks validation that the card belongs to this source. IfactiveCardIdis ever stale or corrupted, it could surface a card from another source within the same guild. Add asourceIdcheck to ensure ownership.🔒 Suggested fix
- if (source?.activeCardId) { - imageCard = await prisma.imageCard.findUnique({ - where: { id: source.activeCardId }, - }); - } + if (source?.activeCardId) { + imageCard = await prisma.imageCard.findFirst({ + where: { + id: source.activeCardId, + sourceId: source.id, + }, + }); + }Note: The same pattern appears in
app/dashboard/[guildId]/leave/image/layout.tsx(lines 24-28).lib/discord/guild.ts-118-171 (1)
118-171: Apply Discord permission overwrites sequentially, not aggregated.Discord's permission algorithm applies channel overwrites in order:
@everyone(deny then allow) → roles (sum denies then allows) → member (deny then allow). The current code aggregates all allow and deny bits across all overwrite levels, then applies them once, which can produce incorrect permissions when denies and allows conflict across levels.For example: if
@everyonedenies a permission, a role allows it, and the member denies it, the current code may incorrectly show it as allowed (because the aggregated allow from the role combines with the aggregated deny from both@everyoneand member). The correct sequential approach applies@everyone's overwrite first (resulting in the permission being denied), then the role's overwrite (allowing it), then the member's overwrite (denying it again).Apply the proposed fix to implement the correct sequential overwrite order.
🔧 Proposed fix (apply overwrites in order)
- let allow = 0n, - deny = 0n; + let allow = 0n, + deny = 0n; // a. Overwrite `@everyone` const channel = this.channels.find( (c) => c.id === channelId, ) as APITextChannel; if (!channel) return permissions; const overwriteEveryone = (channel.permission_overwrites ?? []).find( (ow) => ow.id === this.id && ow.type === 0, ); if (overwriteEveryone) { - deny |= BigInt(overwriteEveryone.deny); - allow |= BigInt(overwriteEveryone.allow); + permissions = + (permissions & ~BigInt(overwriteEveryone.deny)) | + BigInt(overwriteEveryone.allow); } - // b. Overwrites des rôles (somme tous les allow/deny des rôles du membre) - for (const roleId of member.roles) { - const ow = (channel.permission_overwrites ?? []).find( - (ow) => ow.id === roleId && ow.type === 0, - ); - if (ow) { - deny |= BigInt(ow.deny); - allow |= BigInt(ow.allow); - } - } + // b. Overwrites des rôles + let roleAllow = 0n; + let roleDeny = 0n; + for (const roleId of member.roles) { + const ow = (channel.permission_overwrites ?? []).find( + (ow) => ow.id === roleId && ow.type === 0, + ); + if (ow) { + roleDeny |= BigInt(ow.deny); + roleAllow |= BigInt(ow.allow); + } + } + permissions = (permissions & ~roleDeny) | roleAllow; // c. Overwrite utilisateur const overwriteUser = (channel.permission_overwrites ?? []).find( (ow) => ow.id === member.user.id && ow.type === 1, ); if (overwriteUser) { - deny |= BigInt(overwriteUser.deny); - allow |= BigInt(overwriteUser.allow); + permissions = + (permissions & ~BigInt(overwriteUser.deny)) | + BigInt(overwriteUser.allow); } - // Appliquer deny puis allow - permissions = (permissions & ~deny) | allow; return permissions;
🟡 Minor comments (21)
eslint.config.mjs-6-24 (1)
6-24: Moveprettierto the end of the config array.In ESLint flat config, later configurations override earlier ones.
eslint-config-prettierdisables formatting rules that conflict with Prettier. By placing it first, the Next.js configs can re-enable those conflicting rules. Moveprettierafter the Next.js configs to ensure Prettier-conflicting rules stay disabled.Proposed fix
const eslintConfig = defineConfig([ - prettier, ...nextVitals, ...nextTs, + prettier, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: ".next/**", "prisma/**", "generated/**", "node_modules/**", "bower_components/**", "public/**", "static/**", "out/**", "build/**", "next-env.d.ts", ]), ]);app/admin/page.tsx-6-6 (1)
6-6: If the intent is only to force dynamic rendering, useexport const dynamic = "force-dynamic"instead.The code calls
await cookies()without using the result. Whilecookies()is a Dynamic API in Next.js 15 that does opt the route into dynamic rendering, calling it without consuming the value is misleading. If dynamic rendering is the goal but cookies aren't actually needed,export const dynamic = "force-dynamic"is more explicit and clear about the intent.lib/dal.ts-556-565 (1)
556-565: Hardcoded user ID should be configurable.The magic string
"479216487173980160"appears to be a developer/admin bypass. Extract this to an environment variable or configuration constant for maintainability and to avoid exposing privileged user IDs in source code.♻️ Suggested approach
+// At top of file or in config +const ADMIN_USER_ID = process.env.ADMIN_USER_ID; + export const getUserGuild = cache(async (guildId: string) => { const user = await getUser(); if (!user) return null; - if (user.id === "479216487173980160") return await getGuild(guildId); + if (ADMIN_USER_ID && user.id === ADMIN_USER_ID) return await getGuild(guildId);lib/dal.ts-502-502 (1)
502-502: Remove debugconsole.logstatement.This debug logging should be removed before merging to production.
🐛 Proposed fix
if (!data || "message" in data) return null; - console.log("member", data); return data;.gitignore-4-6 (1)
4-6: Avoid ignoring.editorconfig.
Keeping it tracked ensures consistent formatting across editors.💡 Suggested change
-.editorconfigcomponents/layout/sidebar/sidebar-item.tsx-20-49 (1)
20-49: Fix invalid list semantics by moving<li>outside<Link>.
<li>should be a direct child of<ul>/<ol>. Wrapping<li>inside<a>is invalid HTML and can break semantics/accessibility.🧩 Suggested fix
- return ( - <Link - href={link} - onClick={() => { - setIsOpen(false); - }} - > - <li - className={`relative flex items-center justify-center py-2 px-3 my-1 - font-medium rounded-md cursor-pointer - transition-colors group hover:text-indigo-800 - ${active ? "bg-gradient-to-tr from-indigo-200 to-indigo-100 text-indigo-800" : "hover:bg-indigo-50 text-white-600"}`} - > - {icon} - <span - className={`overflow-hidden transition-all sm:block hidden ${ - isOpen ? "w-48 ml-3" : "w-0" - }`} - > - {text} - </span> - - {!isOpen && ( - <div - className={`hidden sm:absolute left-full rounded-md px-2 py-1 ml-6 bg-indigo-100 text-indigo-800 text-sm invisible opacity-20 -translate-x-3 transition-all group-hover:visible group-hover:opacity-100 group-hover:translate-x-0`} - > - {text} - </div> - )} - </li> - </Link> - ); + return ( + <li className="my-1"> + <Link + href={link} + onClick={() => { + setIsOpen(false); + }} + className={`relative flex items-center justify-center py-2 px-3 + font-medium rounded-md cursor-pointer + transition-colors group hover:text-indigo-800 + ${active ? "bg-gradient-to-tr from-indigo-200 to-indigo-100 text-indigo-800" : "hover:bg-indigo-50 text-white-600"}`} + > + {icon} + <span + className={`overflow-hidden transition-all sm:block hidden ${ + isOpen ? "w-48 ml-3" : "w-0" + }`} + > + {text} + </span> + + {!isOpen && ( + <div + className={`hidden sm:absolute left-full rounded-md px-2 py-1 ml-6 bg-indigo-100 text-indigo-800 text-sm invisible opacity-20 -translate-x-3 transition-all group-hover:visible group-hover:opacity-100 group-hover:translate-x-0`} + > + {text} + </div> + )} + </Link> + </li> + );components/dashboard/guild/image-editor/components/avatar-editor.tsx-44-54 (1)
44-54: Addtype="button"to prevent unintended form submission.Native
<button>elements default totype="submit", which can cause unexpected form submissions if this component is used within a form.🛡️ Suggested fix
<button key={color} + type="button" onClick={() => onAvatarBorderColorChange(color)}components/dashboard/guild/editor/image-position-editor.tsx-36-36 (1)
36-36: Remove debugconsole.logstatement.This appears to be a debug artifact that should be removed before merging.
🐛 Proposed fix
- console.log("hasImage", hasImage);components/dashboard/guild/editor/editor.tsx-13-13 (1)
13-13: Add null check for user before using non-null assertion.
getUser()may returnnullorundefined, but line 35 usesuser!.toObject()with a non-null assertion. While the parent layout may handle authentication redirects, adding a defensive check here improves robustness if the component is used in different contexts.🛡️ Proposed fix
export async function Editor({ guild }: { guild: Guild }) { const user = await getUser(); + if (!user) { + throw new Error("User not authenticated"); + } const channels = await guild.getChannels(); return ( ... <EditorMessagePreview guild={guild.toObject()} - user={user!.toObject()} + user={user.toObject()} />Also applies to: 35-35
components/dashboard/guild/editor/send-menu.tsx-96-96 (1)
96-96: Addkeyto the Fragment or remove unnecessary Fragment wrapper.The Fragment
<>wrappingSelectSectioninside a.map()doesn't have akeyprop and the Fragment itself is unnecessary sinceSelectSectionalready has a key. React will warn about missing keys in lists.🐛 Proposed fix
- return ( - <> - <SelectSection - key={channel.id} - showDivider - title={channel.name} - > + return ( + <SelectSection + key={channel.id} + showDivider + title={channel.name} + > {children.map((c) => ( ... ))} - </SelectSection> - </> - ); + </SelectSection> + );lib/discord/image.ts-20-22 (1)
20-22: Canvas lookup may silently fail.If the canvas element with id
"preview-canvas"doesn't exist in the DOM when this function is called,canvaswill benulland the build will proceed withundefined. Consider adding explicit error handling or a warning to help debug rendering issues.🛡️ Proposed defensive check
const canvas = document.getElementById( "preview-canvas", ) as HTMLCanvasElement | null; + + if (!canvas) { + console.warn("Preview canvas element not found in DOM"); + }components/dashboard/guild/image-editor/components/background-editor.tsx-145-153 (1)
145-153: Image preview may remain hidden after error recovery.When the image fails to load,
display: noneis set directly on the element. However, if the user subsequently enters a valid URL, the preview will remain hidden because the display style isn't reset. Consider using React state to control visibility instead.🐛 Proposed fix using React state
Add state at the component level:
const [imageError, setImageError] = useState(false); // Reset error state when URL changes useEffect(() => { setImageError(false); }, [backgroundImgURL]);Then update the image rendering:
- {backgroundImgURL && ( + {backgroundImgURL && !imageError && ( <div className="relative rounded-lg overflow-hidden border-2 border-default-200"> {/* eslint-disable-next-line `@next/next/no-img-element` */} <img src={backgroundImgURL} alt="Background preview" id="card-bg" className="w-full h-32 object-cover" - onError={(e) => { - e.currentTarget.style.display = "none"; - }} + onError={() => setImageError(true)} /> </div> )}app/dashboard/[guildId]/page.tsx-47-48 (1)
47-48: Validate Period enum values from search params.The search params are cast directly to
Periodwithout validation. If a user provides an invalid period value in the URL (e.g.,?welcomerPeriod=invalid), the invalid string will be passed toStatsViewer, potentially causing runtime errors or unexpected behavior.🛡️ Proposed validation
- const welcomerPeriod = (search.welcomerPeriod as Period) || Period.DAILY; - const leaverPeriod = (search.leaverPeriod as Period) || Period.DAILY; + const isValidPeriod = (value: string | undefined): value is Period => + value !== undefined && Object.values(Period).includes(value as Period); + + const welcomerPeriod = isValidPeriod(search.welcomerPeriod) + ? search.welcomerPeriod + : Period.DAILY; + const leaverPeriod = isValidPeriod(search.leaverPeriod) + ? search.leaverPeriod + : Period.DAILY;app/(home)/status/page.tsx-31-37 (1)
31-37: Timer can display negative values.The countdown timer decrements unconditionally every second without a lower bound check. Between the time it reaches 0 and the next status fetch (which resets it to 20), users may see negative countdown values like "Updating in -1 seconds".
🛠️ Proposed fix
useEffect(() => { const interval = setInterval(() => { - setUpdateTime((prev) => prev - 1); + setUpdateTime((prev) => (prev > 0 ? prev - 1 : 0)); }, 1000); // Update every second return () => clearInterval(interval); }, []);lib/actions.ts-117-134 (1)
117-134: UseforEach()instead ofmap()for side-effect-only iterations.The
map()callbacks on lines 117-123 and 125-134 don't return values, which is a misuse ofmap(). This was also flagged by static analysis. UseforEach()when you only need side effects.🛠️ Proposed fix
- store.message.embeds?.map((embed, i: number) => { + store.message.embeds?.forEach((embed, i: number) => { if (embed.image && embed.image.url === "imageCard") { // Remove image before validation embed.image = undefined; embedIndex = i; } }); new MessageBuilder(store.message).toJSON(); - store.message.embeds?.map((embed, i: number) => { + store.message.embeds?.forEach((embed, i: number) => { if ( embedIndex !== undefined && i === embedIndex && store.imagePosition === "embed" ) { // Restore image after validation embed.image = { url: "attachment://card.png" }; } });lib/actions.ts-109-112 (1)
109-112: Redundant assignment and missing error handling for card deletion.Line 111 sets
store.imagePosition = undefinedwhen the condition already ensures it'sundefined. Additionally, the result ofdeleteActiveImageCardInternalis not checked - if deletion fails, the function continues without notifying the caller.🛠️ Proposed fix
if (store.imageEmbedIndex == undefined && store.imagePosition === undefined) { - await deleteActiveImageCardInternal(sourceId, guildId); - store.imagePosition = undefined; + const deleteResult = await deleteActiveImageCardInternal(sourceId, guildId); + if (!deleteResult.done && deleteResult.error !== "No active card to delete") { + console.error("Failed to delete active card:", deleteResult.error); + } }app/dashboard/[guildId]/leave/page.tsx-17-18 (1)
17-18: Fix incorrect header text - says "Welcome" instead of "Leaver".Line 18 displays "Welcome module status" but this is the Leaver page. This appears to be a copy-paste error from the Welcomer page.
🐛 Suggested fix
return ( <CardHeader className="flex justify-between"> - <p>Welcome module status</p> + <p>Leaver module status</p> {leaverParams ? (components/dashboard/guild/editor/embed/author/embed-author-url.tsx-24-29 (1)
24-29: Remove unnecessary type assertion—it's redundant with optional chaining.The cast
(embed?.author as APIEmbedAuthor)is unnecessary sinceembed?.authoris already properly typed asAPIEmbedAuthor | undefined. Remove the assertion:onValueChange={(value) => editEmbed(embedIndex, { ...embed, - author: { ...(embed?.author as APIEmbedAuthor), url: value }, + author: { ...embed?.author, url: value }, }) }Note: Spreading the entire
embedobject is necessary becauseeditEmbedperforms a full replacement of the embed, not a shallow merge. Omitting it would lose other embed properties.components/dashboard/guild/image-editor/components/preview.tsx-25-33 (1)
25-33: Fix loading render condition (nullish coalescing prevents skeleton).Line 25 uses
??, sotruerenders nothing and the skeleton never appears. Use a ternary or&&.✅ Proposed fix
- { - isLoading ?? ( - <Skeleton> - <div className="flex justify-center items-center" style={{width: "800px", height: "300px"}}> - Loading - </div> - </Skeleton> - ) - } + {isLoading ? ( + <Skeleton> + <div + className="flex justify-center items-center" + style={{ width: "800px", height: "300px" }} + > + Loading + </div> + </Skeleton> + ) : null}app/dashboard/[guildId]/welcome/image/layout.tsx-31-36 (1)
31-36: Remove or guard the debug console.log.This will run on every request and can add noise (and leak IDs) in production logs. Consider removing it or gating by environment.
🧹 Suggested change
- console.log("ImageCardStoreProvider - Welcome Image Layout", { - hasSource: !!source, - sourceId: source?.id, - hasImageCard: !!imageCard, - imageCardId: imageCard?.id, - }); + if (process.env.NODE_ENV !== "production") { + console.log("ImageCardStoreProvider - Welcome Image Layout", { + hasSource: !!source, + sourceId: source?.id, + hasImageCard: !!imageCard, + imageCardId: imageCard?.id, + }); + }lib/discord/guild.ts-173-181 (1)
173-181: Avoid flippingbetabefore persistence succeeds.
If the DAL call fails, local state becomes inconsistent. Updatebetaonly on success.🔧 Proposed fix
public async enrollToBetaProgram() { - this.beta = true; - return await addGuildToBeta(this.id); + const ok = await addGuildToBeta(this.id); + if (ok) this.beta = true; + return ok; } public async removeFromBetaProgram() { - this.beta = false; - return await removeGuildToBeta(this.id); + const ok = await removeGuildToBeta(this.id); + if (ok) this.beta = false; + return ok; }
| return ( | ||
| <Input | ||
| type="color" | ||
| value={embedColor ?? undefined} | ||
| onValueChange={(value) => | ||
| editEmbed(embedIndex, { | ||
| ...embed, | ||
| color: value ? parseInt(value.replace("#", ""), 16) : undefined, | ||
| }) | ||
| } | ||
| label="Embed color" | ||
| aria-label="Embed color" | ||
| isClearable | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
@heroui input component isClearable color type documentation
💡 Result:
HeroUI Input component docs (props you asked about):
isClearable:boolean(default:false). Adds a clear button at the end of the input; it shows when the input has a value. [1]color:"default" | "primary" | "secondary" | "success" | "warning" | "danger"(default:"default"). [1]type:"text" | "email" | "url" | "password" | "tel" | "search" | "file"(default:"text"). [1]
Source: HeroUI Input docs page. [1]
References
- HeroUI Docs – Input component: https://www.heroui.com/docs/components/input [1]
Replace unsupported type="color" with a standard input type or use a custom color picker component.
The @heroui/input component does not support type="color" — its supported types are limited to: "text", "email", "url", "password", "tel", "search", and "file". The current implementation will not render correctly. Consider using a native HTML color input (<input type="color" />) directly or switching to a documented custom color picker component.
🤖 Prompt for AI Agents
In `@components/dashboard/guild/editor/embed/body/embed-body-color-input.tsx`
around lines 18 - 31, The `@heroui` Input does not support type="color", so
replace the Input usage with a native <input type="color"> (or a documented
color picker) and wire its value/onChange to the existing editEmbed logic: use
embedColor ? `#${embedColor.toString(16).padStart(6,'0')}` for the input value,
onChange should call editEmbed(embedIndex, {...embed, color: e.target.value ?
parseInt(e.target.value.replace("#",""), 16) : undefined}); remove the
unsupported isClearable prop and, if you need clear behavior, add a small clear
button that calls editEmbed(..., { color: undefined }) alongside the color
input. Ensure you reference the same identifiers (editEmbed, embedIndex, embed,
embedColor) when making the changes.
| await generateImage(config, guildId).then( | ||
| () => { | ||
| setIsLoading(false); | ||
| } | ||
| ) | ||
| console.log("Image generated successfully!"); | ||
| } catch (error) { | ||
| console.error("Error generating image:", error); | ||
| setError( | ||
| error instanceof Error ? error.message : "Failed to generate image", | ||
| ); | ||
| } |
There was a problem hiding this comment.
isLoading is never reset to false when an error occurs.
If generateImage throws, the catch block sets the error but never calls setIsLoading(false). This leaves the UI in a permanent loading state after a failure.
Additionally, the mix of await with .then() is confusing—the console.log on line 24 may execute before the .then() callback.
🐛 Proposed fix
try {
console.log("Generating image...");
setIsLoading(true);
setError(null);
- await generateImage(config, guildId).then(
- () => {
- setIsLoading(false);
- }
- )
+ await generateImage(config, guildId);
console.log("Image generated successfully!");
+ setIsLoading(false);
} catch (error) {
console.error("Error generating image:", error);
setError(
error instanceof Error ? error.message : "Failed to generate image",
);
+ setIsLoading(false);
}📝 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.
| await generateImage(config, guildId).then( | |
| () => { | |
| setIsLoading(false); | |
| } | |
| ) | |
| console.log("Image generated successfully!"); | |
| } catch (error) { | |
| console.error("Error generating image:", error); | |
| setError( | |
| error instanceof Error ? error.message : "Failed to generate image", | |
| ); | |
| } | |
| await generateImage(config, guildId); | |
| console.log("Image generated successfully!"); | |
| setIsLoading(false); | |
| } catch (error) { | |
| console.error("Error generating image:", error); | |
| setError( | |
| error instanceof Error ? error.message : "Failed to generate image", | |
| ); | |
| setIsLoading(false); | |
| } |
🤖 Prompt for AI Agents
In `@components/dashboard/guild/image-editor/hooks/use-image-editor.ts` around
lines 19 - 30, The hook's image generation path mixes await with .then() and
fails to clear loading on errors: when calling generateImage (function
generateImage in use-image-editor hook) ensure you await it without chaining
.then(), call setIsLoading(false) in a finally block (or after try/catch) so
both success and error paths reset loading, and keep the console.log after the
awaited call; also keep the catch setting setError(error instanceof Error ?
error.message : "Failed to generate image") as is.
| guildId: string; | ||
| module: SourceType; | ||
| }) { | ||
| const link = `/dashboard/${guildId}/${module.slice(0, -1).toLowerCase()}`; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find SourceType enum/type definition to understand the values
rg "enum SourceType|type SourceType|interface SourceType" -A 15 --type ts --type tsxRepository: Welcomer-Bot/welcomer-client
Length of output: 99
🏁 Script executed:
# Also search in generated Prisma files which is where it's imported from
find . -path "*/generated/prisma/*" -name "*.ts" -o -name "*.d.ts" | head -20Repository: Welcomer-Bot/welcomer-client
Length of output: 54
🏁 Script executed:
# Search for the actual import and file location
rg -n "SourceType" -B 2 -A 2 components/dashboard/guild/manage-button.tsxRepository: Welcomer-Bot/welcomer-client
Length of output: 402
🏁 Script executed:
# Find the prisma browser generated file
find . -path "*generated/prisma/browser*" -type fRepository: Welcomer-Bot/welcomer-client
Length of output: 54
🏁 Script executed:
# Search for SourceType definition without file type restriction
rg "SourceType" --max-count=30 -B 3 -A 15Repository: Welcomer-Bot/welcomer-client
Length of output: 22481
The URL construction logic fails for the "DM" SourceType.
The slice(0, -1) transformation assumes all SourceType values follow a pattern where removing the last character produces the correct URL segment. However, the enum includes DM, which becomes "d" instead of "dm":
"Welcomer"→"welcomer"✓"Leaver"→"leave"✓"DM"→"d"✗
Use an explicit mapping or a more robust transformation that handles all enum values correctly.
🤖 Prompt for AI Agents
In `@components/dashboard/guild/manage-button.tsx` at line 14, The URL-building
line using module.slice(0, -1).toLowerCase() incorrectly transforms the
SourceType "DM" into "d"; replace this fragile transformation with an explicit
mapping from SourceType values to URL segments (e.g., a map object keyed by
module or SourceType) and use that map when computing link (the variable named
link in manage-button.tsx) so "DM" resolves to "dm" while other types (e.g.,
"Welcomer", "Leaver") map to their expected segments; update any references that
compute link from module to use this mapping.
| {shardId !== undefined && ( | ||
| <div className="mt-2"> | ||
| <p className="text-sm text-gray-500">Selected Shard ID: {shardId}</p> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Condition shardId !== undefined is always true.
shardId is typed as number | null, so shardId !== undefined is always true (even when shardId is null). The shard info will display "Selected Shard ID: null" when no shard is found.
🐛 Proposed fix
- {shardId !== undefined && (
+ {shardId !== null && (
<div className="mt-2">
<p className="text-sm text-gray-500">Selected Shard ID: {shardId}</p>
</div>
)}📝 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.
| {shardId !== undefined && ( | |
| <div className="mt-2"> | |
| <p className="text-sm text-gray-500">Selected Shard ID: {shardId}</p> | |
| </div> | |
| )} | |
| {shardId !== null && ( | |
| <div className="mt-2"> | |
| <p className="text-sm text-gray-500">Selected Shard ID: {shardId}</p> | |
| </div> | |
| )} |
🤖 Prompt for AI Agents
In `@components/shared/status/guild-input.tsx` around lines 42 - 46, The
conditional rendering uses shardId !== undefined but shardId is typed number |
null, so change the check to exclude null (e.g., shardId !== null or
Number.isFinite(shardId)) so the "Selected Shard ID" block only renders when
shardId is an actual number; update the JSX conditional around shardId and any
related display to avoid rendering "null" (refer to the shardId variable in
components/shared/status/guild-input.tsx and the surrounding JSX).
| export async function createModuleStats(guildId: string, source: SourceType) { | ||
| Object.values(Period).forEach(async (period) => { | ||
| await createGuildStats(guildId, period, source); | ||
| }); | ||
| Object.values(Period).forEach(async (period) => { | ||
| await createGuildStats(guildId, period, source); | ||
| }); | ||
| } |
There was a problem hiding this comment.
async callbacks in forEach are not awaited.
Using forEach with async callbacks causes fire-and-forget execution. The function returns immediately before all createGuildStats calls complete, potentially causing race conditions or incomplete data.
🐛 Proposed fix using Promise.all
export async function createModuleStats(guildId: string, source: SourceType) {
- Object.values(Period).forEach(async (period) => {
- await createGuildStats(guildId, period, source);
- });
+ await Promise.all(
+ Object.values(Period).map((period) =>
+ createGuildStats(guildId, period, source)
+ )
+ );
}📝 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.
| export async function createModuleStats(guildId: string, source: SourceType) { | |
| Object.values(Period).forEach(async (period) => { | |
| await createGuildStats(guildId, period, source); | |
| }); | |
| Object.values(Period).forEach(async (period) => { | |
| await createGuildStats(guildId, period, source); | |
| }); | |
| } | |
| export async function createModuleStats(guildId: string, source: SourceType) { | |
| await Promise.all( | |
| Object.values(Period).map((period) => | |
| createGuildStats(guildId, period, source) | |
| ) | |
| ); | |
| } |
🤖 Prompt for AI Agents
In `@lib/dal.ts` around lines 165 - 169, createModuleStats uses
Object.values(Period).forEach with an async callback so createGuildStats calls
are not awaited; change createModuleStats to await all calls by mapping Period
values to promises and using Promise.all (or use a for...of loop with await) so
each createGuildStats(guildId, period, source) completes before returning;
reference the createModuleStats function and createGuildStats invocation and
ensure SourceType/Period usage remains unchanged.
| "use client"; | ||
|
|
||
| import { BaseCardParams, DefaultCard } from "@welcomer-bot/card-canvas"; | ||
| import { parseText } from "./text"; | ||
| import { getGuild, getUser } from "../dal"; | ||
| import { cache } from "react"; | ||
| export const generateImage = cache(async (msg: BaseCardParams, guildId: string) => { | ||
| const user = await getUser(); | ||
| const guild = await getGuild(guildId); | ||
| if (guild && user) { | ||
| if (msg.mainText) { | ||
| msg.mainText.content = parseText(msg.mainText.content ?? "", user, guild); | ||
| } | ||
| if (msg.nicknameText) { | ||
| msg.nicknameText.content = parseText( | ||
| msg.nicknameText.content ?? "", | ||
| user, | ||
| guild | ||
| ); | ||
| } | ||
| if (msg.secondText) { | ||
| msg.secondText.content = parseText( | ||
| msg.secondText.content ?? "", | ||
| user, | ||
| guild | ||
| ); | ||
| import {BaseCardConfig, DefaultCard} from "@welcomer-bot/card-canvas"; | ||
| import {cache} from "react"; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
React cache function "use client" client component compatibility React 19
💡 Result:
In React 19, the cache() API from react is only for React Server Components (RSC) and is not compatible with Client Components (files marked with "use client" or imported under a client boundary). (react.dev)
What this means in practice:
- If a module is under a
"use client"boundary, it will run in the browser, andcache()is not available/expected to work there. (react.dev) - Attempting to use
cache()from code that ends up on the client commonly results in runtime errors like “not implemented” in frameworks such as Next.js. (stackoverflow.com)
What to do instead (client side):
- Use client-side memoization/state patterns (
useMemo,useRef, module-level memoization) or a client caching library (e.g., React Query / SWR), rather thanreact’scache()(which is server-only per React docs). (react.dev)
If you’re on Next.js: note that Next.js also has a separate use cache directive feature (server-side “Cache Components”), which is different from react’s cache(). (nextjs.org)
Citations:
- 1: https://react.dev/reference/react/cache?utm_source=openai
- 2: https://react.dev/reference/rsc/use-client?utm_source=openai
- 3: https://stackoverflow.com/questions/76370924/nextjs-error-not-implemented-when-using-cache?utm_source=openai
- 4: https://react.dev/reference/react/cache?utm_source=openai
- 5: https://nextjs.org/docs/app/api-reference/directives/use-cache?utm_source=openai
🏁 Script executed:
cat -n lib/discord/image.tsRepository: Welcomer-Bot/welcomer-client
Length of output: 1314
Remove cache() wrapper from client component — it causes runtime errors.
React's cache function is exclusively for React Server Components. Using it in a "use client" component will cause runtime errors like "not implemented" at execution time. Replace with client-side alternatives: useMemo for memoization, useCallback for function memoization, or client caching libraries like React Query/SWR.
Also remove the debug console.log on line 13.
🤖 Prompt for AI Agents
In `@lib/discord/image.ts` around lines 1 - 4, This client component imports and
uses React's server-only cache() which breaks at runtime; remove the cache
import and any cache(...) wrapper around functions/components in
lib/discord/image.ts and instead use client-side memoization
(useMemo/useCallback) or a client caching library (React Query/SWR) for the same
behavior; also remove the debug console.log statement present in the file.
Ensure you delete the "import {cache} from 'react';" line and replace wrapped
functions with appropriate useMemo/useCallback usage where needed (or leave them
plain if not necessary) and remove the stray console.log call.
| "use server"; | ||
|
|
||
| import { GuildStats, Period, SourceType } from "@/prisma/generated/client"; | ||
| import { GuildStats, Period, SourceType } from "../generated/prisma/client"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check current directory structure and locate generated Prisma client
echo "=== Checking for generated directories ==="
fd -a -t d 'generated' 2>/dev/null | head -20
echo -e "\n=== Checking generated/prisma structure ==="
if [ -d "generated/prisma" ]; then
ls -la generated/prisma/ 2>/dev/null || echo "Directory exists but cannot list"
fi
echo -e "\n=== Checking relative path from lib/dto.ts ==="
# From lib/dto.ts, ../generated/prisma/client would resolve to generated/prisma/client
if [ -d "generated/prisma/client" ]; then
echo "Path 'generated/prisma/client' EXISTS"
ls -la generated/prisma/client/ 2>/dev/null | head -10
else
echo "Path 'generated/prisma/client' NOT FOUND"
fi
echo -e "\n=== Full repository structure relevant to Prisma ==="
find . -type d -name "prisma" 2>/dev/null
find . -path "*generated*prisma*" -type d 2>/dev/null | head -20Repository: Welcomer-Bot/welcomer-client
Length of output: 311
🏁 Script executed:
#!/bin/bash
echo "=== Prisma directory structure ==="
ls -la prisma/ 2>/dev/null || echo "No prisma directory"
echo -e "\n=== Search for schema.prisma ==="
find . -name "schema.prisma" -o -name "*.prisma" 2>/dev/null
echo -e "\n=== Search for GuildStats type definition ==="
rg "GuildStats" --type ts --type tsx -l 2>/dev/null
echo -e "\n=== Check lib/dto.ts current imports ==="
head -10 lib/dto.ts 2>/dev/null
echo -e "\n=== Search for Prisma client generation location ==="
find . -path "*/prisma/*" -type f 2>/dev/null | head -20
echo -e "\n=== Check if index.d.ts or client.d.ts exists in prisma ==="
find . -name "index.d.ts" -o -name "client.d.ts" 2>/dev/nullRepository: Welcomer-Bot/welcomer-client
Length of output: 837
🏁 Script executed:
#!/bin/bash
echo "=== Check .gitignore for generated patterns ==="
cat .gitignore 2>/dev/null | grep -i "generated"
echo -e "\n=== Check prisma/schema.prisma for models ==="
cat prisma/schema.prisma
echo -e "\n=== Search for GuildStats, Period, SourceType in schema ==="
rg "GuildStats|Period|SourceType" prisma/
echo -e "\n=== Check for build scripts ==="
cat package.json 2>/dev/null | grep -A 10 "scripts"Repository: Welcomer-Bot/welcomer-client
Length of output: 4118
Fix the import path—it should be ../generated/prisma not ../generated/prisma/client.
The Prisma schema specifies output = "../generated/prisma", but the import targets ../generated/prisma/client. Remove the /client segment from the import path to match the configured generator output location.
🤖 Prompt for AI Agents
In `@lib/dto.ts` at line 3, Update the import in lib/dto.ts to match the Prisma
generator output: change the module import for the types GuildStats, Period, and
SourceType from "../generated/prisma/client" to "../generated/prisma" so it
references the configured output directory and resolves the generated types
correctly.
…ent into transform_to_json
… components for improved performance and readability
…Preview, PeriodSelector, and StatsViewer components
Summary by CodeRabbit
Release Notes
New Features
Improvements
Dependencies
✏️ Tip: You can customize this high-level summary in your review settings.