Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Please double check the following review of the pull request:
Changes in the diff
Identified Issues
ID 1: store-cards API did not accept or validate
|
There was a problem hiding this comment.
Pull request overview
This PR implements a complete redesign of the user flow, transitioning from a multi-step wizard-based generator to a direct search-to-profile approach. The changes include a new user page editor with auto-save functionality, support for disabled cards and gradient colors, and comprehensive test updates.
Key Changes:
- Refactored user flow from generator wizard to direct profile editing
- Added new user auto-setup on first visit
- Implemented auto-save for card configurations
- Added support for disabled cards and gradient colors
- Removed legacy generator and settings E2E tests
Reviewed changes
Copilot reviewed 80 out of 81 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
tests/unit/api/store-cards/store-cards.test.ts |
Added tests for disabled cards, backfilling, and global settings optimization |
tests/unit/api/card/card.test.ts |
Added tests for color override and custom preset handling |
tests/e2e/settings/settings.spec.ts |
Removed (deprecated settings page) |
tests/e2e/home/home.spec.ts |
Updated to navigate to search instead of opening generator |
tests/e2e/generator/*.spec.ts |
Removed (deprecated generator wizard) |
tests/e2e/fixtures/test-utils.ts |
Removed GeneratorPage helper |
package.json |
Added new dependencies: @radix-ui/react-alert-dialog, @radix-ui/react-tabs, katex |
lib/utils.ts |
Added gradient color validation support |
lib/types/records.ts |
Added disabled, useCustomSettings fields; made variation optional; added GlobalCardSettings |
lib/stores/user-preferences.ts |
Removed defaultUsername field |
lib/stores/user-page-editor.ts |
New comprehensive state management for user page editing |
lib/stores/index.ts |
Updated exports for new store structure |
lib/stores/card-settings.ts |
Removed (replaced by user-page-editor) |
lib/seo.ts |
Removed settings and license page SEO configs |
lib/card-info-tooltips.ts |
New file with card tooltips and LaTeX formula support |
lib/card-groups.ts |
Updated to support gradient colors in URL params |
lib/card-data/config.ts |
Updated color resolution logic for custom presets |
lib/api-utils.ts |
Added disabled field validation |
hooks/useStatCardSubmit.ts |
Removed (deprecated generator hook) |
hooks/useCardAutoSave.ts |
New auto-save hook with debouncing |
components/user/UserPageHeader.tsx |
New header component with save state indicators |
components/user/UserPageEditor.tsx |
New comprehensive user page editor |
components/user/UserPageClient.tsx |
Removed (replaced by UserPageEditor) |
components/user/UserCard.tsx |
Removed (functionality moved to CardTile) |
components/user/GlobalSettingsPanel.tsx |
New global settings panel |
components/ui/Tabs.tsx |
New tabs component wrapper |
components/search/SearchHeroSection.tsx |
Updated description to reflect new auto-setup flow |
I have completed my review. No critical issues were found that require comments. The changes are well-structured and follow the project's patterns. The test coverage has been updated appropriately to reflect the new user flow, though the removal of E2E tests for the generator means that the new user page editor flow relies primarily on unit tests.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Important Review skippedToo many files! This PR contains 271 files, which is 121 over the limit of 150. ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (271)
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughReplaces the legacy generator and settings with a new User Page Editor and editor store, adds per-card/global settings UI and tooling (tiles, previews, bulk actions, downloads, virtualized/reorderable grids), introduces lightbox/gallery and math rendering, Radix UI primitives, color/gradient utilities, expanded API validation/merge logic, and removes activityHeatmap/recentActivityFeed assets plus many generator/settings pages and tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant Editor as Browser (Editor UI)
participant API as /api/store-cards
participant DB as Persistence
participant AniList as External AniList
Note over User,Editor: User edits cards or triggers new-user setup
User->>Editor: modify cards / toggle settings / request save or setup
Editor->>Editor: debounce & build payload (useCardAutoSave)
Editor->>API: POST /api/store-cards { cards, globalSettings, ifMatch }
API->>API: validateCardData() → sanitize, merge with existing, compute mergedGlobalSettings
API->>DB: persist CardsRecord (cards + mergedGlobalSettings)
DB-->>API: ack + updatedAt
API-->>Editor: 200 OK + stored snapshot
Editor->>Editor: hydrate editor store (useUserPageEditor) and clear dirty state
alt New user onboarding
Editor->>AniList: fetch user id & stats (useNewUserSetup)
AniList-->>Editor: user id & stats
Editor->>API: POST create user & initial cards
API->>DB: persist setup snapshot
DB-->>API: ack
API-->>Editor: setup success
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
@gooroo-dev review |
Please double check the following review of the pull request:
Changes in the diff
Identified Issues
Issue 1: Disabled field type validation missing in store-cards APIExplanation: Fix: Add validation to reject non-boolean // In POST handler, after parsing body.cards:
for (const card of incomingCards as StoredCardConfig[]) {
if ("disabled" in card && typeof card.disabled !== "boolean") {
return new NextResponse(
JSON.stringify({ error: "Invalid 'disabled' field type" }),
{ status: 400 },
);
}
}Explanation of fix: Missing Tests
SummaryThe diff introduces a new user flow with a revamped user page editor, removes legacy generator components and settings page, improves API handling of card configs including disabled cards and global settings, and adds new UI components for card management and bulk actions. It also improves UX in search and example pages by navigating to Summon me to re-review when updated! Yours, Gooroo.dev |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/stat-card-generator/ColorPickerGroup.tsx (2)
315-327: Inconsistent use ofstopsvslocalStopsfor disabled state.The "Add Stop" button's
disabledprop (line 320) usesstops.lengthinstead oflocalStops.length. Since the component now renders and operates onlocalStops, this should be consistent.🔎 Proposed fix
<Button type="button" variant="outline" size="sm" onClick={addStop} - disabled={stops.length >= 5} + disabled={localStops.length >= 5} aria-label={`Add gradient color stop ${stops.length + 1} of 5`}
410-421: Same inconsistency for remove button disabled state.Line 415 uses
stops.lengthfor the remove button's disabled check, but should uselocalStops.lengthfor consistency with the refactored state management.🔎 Proposed fix
<Button type="button" variant="ghost" size="sm" onClick={() => removeStop(index)} - disabled={stops.length <= 2} + disabled={localStops.length <= 2} aria-label={`Remove gradient stop ${index + 1}`}
🧹 Nitpick comments (14)
components/search/SearchForm.tsx (1)
73-105: Consider handling navigation edge cases.The loading state is set to
truebut never reset tofalse. While this works for successful navigation (component unmounts), ifrouter.pushfails silently or the user navigates back, the form could remain in a loading state.This is a minor edge case since Next.js client-side navigation is generally reliable, but you could add a safety mechanism if needed.
🔎 Optional: Reset loading state after navigation attempt
// Ensure the loading overlay has a chance to render before navigation. scheduleAfterPaint(() => { router.push(`/user?${params.toString()}`); + // Reset loading state after a brief delay to handle edge cases + // where navigation might not unmount this component + setTimeout(() => { + setLoading(false); + onLoadingChange?.(false); + }, 2000); });components/user/UserPageHeader.tsx (1)
174-195: Consider simplifying the nested conditional className logic.The nested conditionals for save status styling are correct but complex. Consider extracting the logic into a helper function for better readability.
🔎 Proposed refactor
+function getSaveStatusClass(saveState: SaveState): string { + if (saveState.isSaving) { + return "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"; + } + if (saveState.saveError) { + return "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"; + } + if (saveState.isDirty) { + return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"; + } + if (saveState.lastSavedAt) { + return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"; + } + return "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400"; +} <div className={cn( "flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all", - saveState.isSaving && - "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", - saveState.saveError && - "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", - !saveState.isSaving && - !saveState.saveError && - saveState.isDirty && - "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", - !saveState.isSaving && - !saveState.saveError && - !saveState.isDirty && - saveState.lastSavedAt && - "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400", - !saveState.isSaving && - !saveState.saveError && - !saveState.isDirty && - !saveState.lastSavedAt && - "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400", + getSaveStatusClass(saveState), )} >components/user/ColorPreviewCard.tsx (1)
53-75: Hex parsing assumes valid 6-character format.The hex-to-rgba conversion assumes a 6-character hex code. This could produce incorrect results for shorthand hex (e.g.,
#FFF) or invalid values.🔎 Proposed defensive fix
if (opacity < 1) { // Convert hex to rgba - const hex = stop.color.replace("#", ""); + let hex = stop.color.replace("#", ""); + // Expand shorthand hex (e.g., "FFF" -> "FFFFFF") + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } const r = Number.parseInt(hex.substring(0, 2), 16); const g = Number.parseInt(hex.substring(2, 4), 16); const b = Number.parseInt(hex.substring(4, 6), 16);components/user/GlobalSettingsPanel.tsx (1)
50-66: Consider extracting default values to a shared constant.The reset handler uses hardcoded default values. If defaults change elsewhere (e.g., in the store initialization), these could drift out of sync.
🔎 Suggested approach
// In a shared constants file or exported from the store export const DEFAULT_GLOBAL_SETTINGS = { colorPreset: "default", borderEnabled: false, borderColor: "#e4e2e2", borderRadius: 5, advancedSettings: { useStatusColors: true, showPiePercentages: true, showFavorites: true, gridCols: 3, gridRows: 3, }, } as const; // Then use in the handler: const handleResetToDefaults = useCallback(() => { setGlobalColorPreset(DEFAULT_GLOBAL_SETTINGS.colorPreset); // ... etc }, [...]);components/user/CardSettingsDialog.tsx (2)
106-107:localBorderEnabledmay become stale if external state changes.The
useStateinitializer only runs once when the component mounts. IfhasBorderchanges while the dialog is open (e.g., from another source),localBorderEnabledwon't update to reflect the new state.Consider synchronizing with an effect or deriving directly from the config:
🔎 Option: Sync with effect
const [localBorderEnabled, setLocalBorderEnabled] = useState(hasBorder); + // Sync local state when external border state changes + useEffect(() => { + setLocalBorderEnabled(hasBorder); + }, [hasBorder]);
175-182: Consider memoizingadvancedSettingsobject.This object is recreated on every render. While the impact is minimal, memoizing it would prevent unnecessary re-renders of child components that depend on referential equality.
🔎 Proposed fix
- // Build advanced settings for SettingsContent - const advancedSettings = { - useStatusColors: config.advancedSettings.useStatusColors ?? false, - showPiePercentages: config.advancedSettings.showPiePercentages ?? false, - showFavorites: config.advancedSettings.showFavorites ?? false, - gridCols: config.advancedSettings.gridCols ?? 3, - gridRows: config.advancedSettings.gridRows ?? 3, - }; + // Build advanced settings for SettingsContent + const advancedSettings = useMemo( + () => ({ + useStatusColors: config.advancedSettings.useStatusColors ?? false, + showPiePercentages: config.advancedSettings.showPiePercentages ?? false, + showFavorites: config.advancedSettings.showFavorites ?? false, + gridCols: config.advancedSettings.gridCols ?? 3, + gridRows: config.advancedSettings.gridRows ?? 3, + }), + [config.advancedSettings], + );components/user/BulkActionsToolbar.tsx (2)
173-180: Consider providing user feedback on clipboard errors.When the clipboard operation fails (e.g., due to permissions or secure context requirements), the error is only logged to console. Users won't know why the copy didn't work.
🔎 Proposed enhancement
try { await navigator.clipboard.writeText(urls.join("\n")); setCopiedFormat(format); setTimeout(() => setCopiedFormat(null), 2000); } catch (err) { console.error("Failed to copy to clipboard:", err); + // Consider adding a toast notification or visual feedback here + // e.g., toast.error("Failed to copy to clipboard") }
213-219: Consider user feedback for download failures.Similar to the clipboard error, download failures only log to console. Users may not understand why their download didn't complete, especially if partial failures occur (line 209-213 handles this with a warning but no UI feedback).
components/user/SettingsContent.tsx (1)
250-304: Consider wrapping border settings animation in AnimatePresence for proper exit animation.The
motion.divusesexitprop but isn't wrapped inAnimatePresence, so exit animations won't trigger whenborderEnabledbecomes false.🔎 Proposed fix
+import { AnimatePresence } from "framer-motion"; {/* Border Settings - Only shown when border is enabled */} - {borderEnabled && ( + <AnimatePresence> + {borderEnabled && ( <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="space-y-4" > {/* ... content ... */} </motion.div> - )} + )} + </AnimatePresence>tests/e2e/user/user.spec.ts (1)
200-205: Consider using a more specific selector for preview images.Using
page.locator("main").locator("img")is broad and could match unrelated images (avatars, icons). Consider using a more specific selector like[data-testid="card-preview"]if available.components/user/CardTile.tsx (1)
356-370: Consider adding loading/error states for the preview image.The Image component doesn't have an
onErrorhandler. If the preview URL fails to load, users see no feedback.lib/stores/user-page-editor.ts (2)
238-241: Consider defensive handling for missing presets.If
colorPresets[presetName]doesn't exist andcolorPresets.defaultalso doesn't exist, this would return undefined. While unlikely in practice, adding explicit fallback would be safer.🔎 Proposed defensive fallback
function getPresetColors(presetName: string): ColorValue[] { const preset = colorPresets[presetName]; - return preset?.colors || colorPresets.default.colors; + return preset?.colors || colorPresets.default?.colors || ["#fe428e", "#141321", "#a9fef7", "#fe428e"]; }
560-571: Note: DEFAULT_GLOBAL_ADVANCED_SETTINGS is defined after its usage.The constant is used in
processServerCardsat line 466 but defined at line 565. While JavaScript hoisting handles this for const at module level, placing the constant definition before its usage would improve readability.components/user/UserPageEditor.tsx (1)
913-927: Consider debouncing search-triggered group expansion.When
normalizedQuerychanges rapidly (user typing), this effect expands groups on each change. While not breaking, it could cause minor UI churn.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (81)
AGENTS.mdapp/api/store-cards/route.tsapp/examples/page.tsxapp/globals.cssapp/license/page.tsxapp/page.tsxapp/settings/page.tsxapp/sitemap.xml/route.tsapp/user/page.tsxcomponents/AppSidebar.tsxcomponents/MathTooltipContent.tsxcomponents/StatCardGenerator.tsxcomponents/examples/CTASection.tsxcomponents/examples/CategorySection.tsxcomponents/examples/ExampleCard.tsxcomponents/search/SearchForm.tsxcomponents/search/SearchHeroSection.tsxcomponents/settings/DefaultCardSettings.tsxcomponents/settings/DefaultUsername.tsxcomponents/settings/ResetSettings.tsxcomponents/settings/SidebarBehavior.tsxcomponents/settings/ThemePreferences.tsxcomponents/stat-card-generator/AdvancedOptions.tsxcomponents/stat-card-generator/CardPreviewSkeleton.tsxcomponents/stat-card-generator/ColorPickerGroup.tsxcomponents/stat-card-generator/ColorPresetManager.tsxcomponents/stat-card-generator/ColorPresetSelector.tsxcomponents/stat-card-generator/GeneratorContext.tsxcomponents/stat-card-generator/LivePreview.tsxcomponents/stat-card-generator/LivePreviewSkeleton.tsxcomponents/stat-card-generator/StatCardPreview.tsxcomponents/stat-card-generator/StatCardTypeSelection.tsxcomponents/stat-card-generator/UpdateNotice.tsxcomponents/stat-card-generator/UserDetailsForm.tsxcomponents/stat-card-generator/UserFormSkeleton.tsxcomponents/stat-card-generator/WizardNavigation.tsxcomponents/stat-card-generator/constants.tscomponents/ui/AlertDialog.tsxcomponents/ui/Dialog.tsxcomponents/ui/Select.tsxcomponents/ui/Tabs.tsxcomponents/user/BulkActionsToolbar.tsxcomponents/user/CardCategorySection.tsxcomponents/user/CardList.tsxcomponents/user/CardSettingsDialog.tsxcomponents/user/CardSettingsPanel.tsxcomponents/user/CardTile.tsxcomponents/user/ColorPreviewCard.tsxcomponents/user/GlobalSettingsPanel.tsxcomponents/user/SettingsContent.tsxcomponents/user/UserCard.tsxcomponents/user/UserPageClient.tsxcomponents/user/UserPageEditor.tsxcomponents/user/UserPageHeader.tsxhooks/useCardAutoSave.tshooks/useStatCardSubmit.tslib/api-utils.tslib/card-data/config.tslib/card-groups.tslib/card-info-tooltips.tslib/seo.tslib/stores/card-settings.tslib/stores/index.tslib/stores/user-page-editor.tslib/stores/user-preferences.tslib/types/records.tslib/utils.tspackage.jsontests/e2e/examples/examples.spec.tstests/e2e/fixtures/index.tstests/e2e/fixtures/test-utils.tstests/e2e/generator/batch-generation.spec.tstests/e2e/generator/card-generation.spec.tstests/e2e/generator/customization.spec.tstests/e2e/generator/error-scenarios.spec.tstests/e2e/generator/wizard-navigation.spec.tstests/e2e/home/home.spec.tstests/e2e/settings/settings.spec.tstests/e2e/user/user.spec.tstests/unit/api/card/card.test.tstests/unit/api/store-cards/store-cards.test.ts
💤 Files with no reviewable changes (34)
- app/sitemap.xml/route.ts
- components/stat-card-generator/UserFormSkeleton.tsx
- lib/seo.ts
- components/settings/DefaultUsername.tsx
- components/settings/SidebarBehavior.tsx
- components/settings/DefaultCardSettings.tsx
- tests/e2e/generator/batch-generation.spec.ts
- components/stat-card-generator/ColorPresetManager.tsx
- hooks/useStatCardSubmit.ts
- components/stat-card-generator/UserDetailsForm.tsx
- components/settings/ThemePreferences.tsx
- components/user/UserCard.tsx
- app/license/page.tsx
- components/stat-card-generator/WizardNavigation.tsx
- components/StatCardGenerator.tsx
- lib/stores/card-settings.ts
- app/settings/page.tsx
- tests/e2e/generator/error-scenarios.spec.ts
- tests/e2e/generator/card-generation.spec.ts
- components/stat-card-generator/StatCardTypeSelection.tsx
- components/stat-card-generator/AdvancedOptions.tsx
- lib/stores/user-preferences.ts
- components/user/CardList.tsx
- components/stat-card-generator/GeneratorContext.tsx
- components/settings/ResetSettings.tsx
- tests/e2e/generator/wizard-navigation.spec.ts
- tests/e2e/settings/settings.spec.ts
- components/stat-card-generator/LivePreview.tsx
- components/stat-card-generator/LivePreviewSkeleton.tsx
- components/stat-card-generator/UpdateNotice.tsx
- components/stat-card-generator/StatCardPreview.tsx
- tests/e2e/generator/customization.spec.ts
- components/stat-card-generator/CardPreviewSkeleton.tsx
- components/user/UserPageClient.tsx
🧰 Additional context used
🧬 Code graph analysis (26)
components/examples/CTASection.tsx (2)
components/CTASection.tsx (1)
CTASection(29-136)components/examples/index.ts (1)
CTASection(4-4)
tests/e2e/home/home.spec.ts (1)
tests/e2e/fixtures/test-utils.ts (1)
test(26-143)
components/ui/Tabs.tsx (1)
lib/utils.ts (1)
cn(178-180)
components/examples/ExampleCard.tsx (1)
components/examples/index.ts (1)
ExampleCard(3-3)
components/user/GlobalSettingsPanel.tsx (3)
lib/types/card.ts (1)
ColorValue(41-41)components/ui/Dialog.tsx (3)
DialogHeader(191-191)DialogTitle(193-193)DialogDescription(194-194)components/user/CardSettingsPanel.tsx (1)
CardSettingsPanel(64-182)
components/ui/Dialog.tsx (1)
lib/utils.ts (1)
cn(178-180)
app/user/page.tsx (1)
components/user/UserPageEditor.tsx (1)
UserPageEditor(605-1543)
lib/card-data/config.ts (3)
lib/card-data/index.ts (1)
isCustomPreset(48-48)lib/types/records.ts (2)
StoredCardConfig(445-462)CardsRecord(482-487)lib/utils.ts (1)
clampBorderRadius(870-877)
components/user/CardCategorySection.tsx (1)
lib/utils.ts (1)
cn(178-180)
tests/unit/api/card/card.test.ts (4)
tests/unit/__setup__.test.ts (1)
sharedRedisMockGet(15-15)app/api/card/route.ts (1)
GET(535-660)lib/svg-templates/media-stats/shared.ts (1)
mediaStatsTemplate(242-382)lib/svg-templates/extra-anime-manga-stats/shared.ts (1)
extraAnimeMangaStatsTemplate(39-661)
components/ui/AlertDialog.tsx (2)
lib/utils.ts (1)
cn(178-180)components/ui/Button.tsx (1)
buttonVariants(73-73)
components/user/CardSettingsPanel.tsx (4)
components/user/SettingsContent.tsx (1)
SettingsContent(108-460)lib/utils.ts (1)
cn(178-180)components/ui/Button.tsx (1)
Button(73-73)components/ui/Switch.tsx (1)
Switch(34-34)
hooks/useCardAutoSave.ts (4)
lib/types/card.ts (1)
ColorValue(41-41)lib/stores/user-page-editor.ts (1)
ServerCardData(190-207)lib/stores/index.ts (1)
ServerCardData(31-31)lib/utils.ts (2)
parseResponsePayload(631-644)getResponseErrorMessage(651-665)
components/ui/Select.tsx (1)
lib/utils.ts (1)
cn(178-180)
lib/types/records.ts (1)
lib/types/card.ts (1)
ColorValue(41-41)
app/api/store-cards/route.ts (3)
lib/card-data/validation.ts (1)
displayNames(79-134)lib/types/records.ts (3)
StoredCardConfig(445-462)GlobalCardSettings(465-479)CardsRecord(482-487)lib/utils.ts (2)
safeParse(769-802)clampBorderRadius(870-877)
app/examples/page.tsx (2)
components/examples/CTASection.tsx (1)
CTASection(11-49)components/CTASection.tsx (1)
CTASection(29-136)
components/search/SearchForm.tsx (1)
lib/utils/google-analytics.ts (3)
safeTrack(95-101)trackFormSubmission(222-228)trackNavigation(181-192)
components/user/BulkActionsToolbar.tsx (4)
lib/stores/user-page-editor.ts (1)
useUserPageEditor(599-1110)hooks/useIsMobile.ts (1)
useIsMobile(12-31)lib/card-groups.ts (2)
mapStoredConfigToCardUrlParams(458-546)buildCardUrlWithParams(599-647)lib/utils.ts (3)
ConversionFormat(152-152)BatchExportCard(155-159)batchConvertAndZip(1180-1221)
tests/e2e/user/user.spec.ts (2)
tests/e2e/fixtures/test-utils.ts (2)
expect(182-182)test(26-143)tests/e2e/fixtures/mock-data.ts (1)
mockCardsRecord(254-277)
tests/unit/api/store-cards/store-cards.test.ts (3)
tests/unit/__setup__.test.ts (3)
sharedRatelimitMockLimit(65-67)sharedRedisMockSet(16-16)sharedRedisMockGet(15-15)app/api/store-cards/route.ts (1)
POST(357-496)lib/card-data/validation.ts (1)
displayNames(79-134)
components/user/UserPageEditor.tsx (7)
lib/anilist/queries.ts (2)
USER_ID_QUERY(2-8)USER_STATS_QUERY(529-536)lib/utils.ts (2)
parseResponsePayload(631-644)getResponseErrorMessage(651-665)lib/error-messages.ts (1)
getErrorDetails(316-359)lib/error-tracking.ts (1)
trackUserActionError(73-92)lib/types/records.ts (1)
ReconstructedUserRecord(432-442)lib/stores/user-page-editor.ts (3)
ServerCardData(190-207)ServerGlobalSettings(213-227)useUserPageEditor(599-1110)hooks/useCardAutoSave.ts (1)
useCardAutoSave(174-260)
components/stat-card-generator/ColorPickerGroup.tsx (1)
lib/types/card.ts (1)
GradientStop(5-14)
components/user/UserPageHeader.tsx (2)
components/PageShell.tsx (2)
containerVariants(13-19)itemVariants(21-24)lib/utils.ts (1)
cn(178-180)
lib/card-groups.ts (2)
lib/utils.ts (1)
buildApiUrl(57-62)lib/types/card.ts (1)
ColorValue(41-41)
lib/utils.ts (1)
lib/svg-templates/common/color-utils.ts (1)
isValidHexColor(4-4)
🪛 ast-grep (0.40.3)
components/MathTooltipContent.tsx
[warning] 35-35: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
components/user/ColorPreviewCard.tsx
[warning] 153-153: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
components/MathTooltipContent.tsx
[error] 36-36: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
components/user/ColorPreviewCard.tsx
[error] 154-154: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🪛 LanguageTool
AGENTS.md
[style] ~20-~20: Try moving the adverb to make the sentence clearer.
Context: ... task — DO NOT create one Plan that attempts to fully plan multiple unrelated tasks at once. Each Plan must be focused and ...
(SPLIT_INFINITIVE)
🔇 Additional comments (103)
components/stat-card-generator/ColorPresetSelector.tsx (3)
18-18: LGTM: ChevronUp import added for toggle UI.The import is correctly added to support the expand/collapse toggle functionality introduced later in the component.
404-404: LGTM: Consistent border radius on selection overlay.The
rounded-xlclass ensures the selection overlay respects the parent button's rounded corners, preventing visual artifacts at the edges.
466-493: LGTM: Well-implemented toggle behavior for expand/collapse.The toggle implementation correctly:
- Uses
hasMorePresetsto conditionally render the button- Toggles
isExpandedstate bidirectionally- Provides clear visual feedback with appropriate chevron icons
- Displays intuitive button text ("Show less" vs. "Show all X presets")
This is a solid UX improvement over the previous expand-only behavior, allowing users to collapse the list when needed.
components/search/SearchHeroSection.tsx (1)
67-69: LGTM!The updated onboarding text clearly communicates the new user flow—directing users to enter a username/ID and noting automatic setup for first-time users. This aligns well with the PR's shift from modal-based generation to direct navigation.
tests/e2e/fixtures/index.ts (1)
1-6: LGTM!Clean simplification of the fixture exports following the removal of the GeneratorPage Page Object. The updated comment accurately reflects the reduced scope.
lib/utils.ts (1)
472-511: LGTM with a minor observation on naming semantics.The JSON-encoded gradient support is a good addition for handling stored/transported gradient definitions. The trimming and empty string checks are solid defensive improvements.
One minor semantic note:
getColorInvalidReasonreturning"gradient JSON string is valid"(line 497) when the value is actually valid may be slightly confusing since the function name implies it returns reasons for invalid values. However, looking at the overall pattern, this seems intentional as a diagnostic message for edge cases where shared validation differs from individual checks. No action required.components/AppSidebar.tsx (1)
33-40: LGTM!The new "Search User" navigation item is well-structured with consistent styling patterns (gradient, bgLight, textColor). The
/searchroute aligns with the PR's shift to route-based user discovery.tests/e2e/fixtures/test-utils.ts (1)
1-21: LGTM!Clean removal of the
GeneratorPagePage Object and associated imports. The remaining mock fixtures (mockSuccessfulApi,mockRateLimitedApi,mockUserNotFoundApi,mockNetworkError) are well-organized and sufficient for testing the new route-based flow.components/search/SearchForm.tsx (1)
25-33: Nice paint-scheduling pattern.The
scheduleAfterPainthelper withrequestAnimationFrameandsetTimeoutfallback is a clean approach to ensure the loading overlay renders before navigation.components/ui/Tabs.tsx (1)
1-55: LGTM!Well-structured Radix UI Tabs wrapper following established patterns in the codebase. Good use of
forwardReffor all subcomponents, properdisplayNameassignments, and comprehensive styling including dark mode and focus-visible states.components/stat-card-generator/ColorPickerGroup.tsx (1)
238-244: Good local state pattern for controlled edits.Using
localStopsto manage edits locally before propagating viaonStopsChangeis a clean pattern that avoids prop mutation and provides better control over state updates.components/ui/Dialog.tsx (2)
52-64: LGTM! Clean API extension.The new
DialogContentPropsinterface provides a clear escape hatch for hiding or customizing the close button, with good documentation.
71-110: LGTM! Conditional close button rendering is correctly implemented.The implementation properly:
- Destructures the new props with sensible defaults
- Conditionally renders the close button based on
hideCloseButton- Applies custom styling via
closeButtonClassNamewhen provided- Maintains all existing functionality
AGENTS.md (1)
1-55: LGTM! Documentation restructure improves clarity.The refactored workflow documentation is more actionable with:
- Clear quick-start steps
- Concrete subagent workflow example
- Better organized sections for modes, libraries, and memory policy
The static analysis hint about the split infinitive ("to fully plan") is a minor style preference that doesn't impact clarity.
components/user/UserPageHeader.tsx (2)
64-71: LGTM! Time formatting logic is clear and correct.The function properly handles null input and provides sensible time buckets (5s, 60s, 3600s) with appropriate formatting.
114-122: Theunoptimizedprop is necessary and should be retained. AniList avatar domains are not configured innext.config.ts—only internal domains (api.anicards.alpha49.com, anicards.alpha49.com, and localhost variants) are inremotePatterns. Without explicit configuration, Next.js image optimization would fail for AniList avatar URLs. Theunoptimizedprop is the correct approach for external images from unconfigured domains.Likely an incorrect or invalid review comment.
components/ui/Select.tsx (3)
56-57: LGTM! Improved scroll button interactivity.Changing from
cursor-defaulttocursor-pointerand enhancing hover states provides better UX feedback for interactive scroll buttons.Also applies to: 74-75
97-97: LGTM! Enhanced visual elevation for dropdown.The updated styling with rounded-xl, backdrop blur, and theme-aware borders/shadows creates a more polished, modern dropdown appearance.
151-163: LGTM! Comprehensive item state styling.The updated SelectItem styling properly handles:
- Hover/focus states with appropriate backgrounds
- Checked state with gradient accent
- Disabled state with reduced opacity
- Theme-aware colors for light/dark modes
hooks/useCardAutoSave.ts (5)
45-68: LGTM! Proper error handling in API call.The function correctly:
- Uses appropriate headers and body formatting
- Parses response payload using provided utilities
- Handles both network errors and HTTP errors
- Returns consistent result shape
184-192: LGTM! Proper cleanup on unmount.The effect correctly:
- Sets mounted flag to true on mount
- Returns cleanup function to clear timeouts and set mounted flag to false
- Prevents state updates after unmount
229-250: LGTM! Correct debounce implementation.The auto-save effect properly:
- Guards against disabled state, missing userId, no changes, or active save
- Clears existing timeout before setting new one
- Returns cleanup function
- Lists all relevant dependencies
94-110: No action needed. The client-side color logic inuseCardAutoSave.tsis correctly aligned with the server-side handler in/api/store-cards/route.ts. Both use identical conditionals (!colorPreset || colorPreset === "custom") to determine whether to send individual colors, and the server properly handles undefined color fields by omitting them from storage. The design is intentional: when a preset is selected, the preset name is the source of truth and individual colors are not stored.
115-150: The merge logic is correct and properly aligned with server-side expectations. The code correctly handles custom vs global color preset selection (client sendseffectivePresetwhich server mirrors inbuildCardConfig), conditionally sends colors only when applicable (shouldSendColorslogic), preserves per-card advanced settings through the merge process, and correctly handles the disabled flag. No changes needed.lib/card-info-tooltips.ts (3)
9-9: Shannon entropy formula appears correct.The normalized Shannon entropy formula
H' = -Σ(p_i ln p_i) / ln kis the standard formula for diversity indices. The use ofString.rawproperly preserves backslashes in LaTeX notation.Also applies to: 11-11, 13-13
77-89: LGTM! Simple and effective helper functions.Both helpers provide clean APIs for querying tooltip availability and content.
7-14: KaTeX is properly configured and rendering LaTeX formulas correctly.KaTeX (v0.16.27) is installed, with CSS imported globally in
app/globals.css. TheMathTooltipContentcomponent handles rendering of both inline ($...$) and display ($$...$$) math delimiters usingkatex.renderToString()with appropriate error handling. This component is integrated with the tooltip system and receives content fromCARD_INFO_TOOLTIPS.tests/e2e/examples/examples.spec.ts (1)
38-47: LGTM! Test correctly validates route-based navigation.The updated test properly:
- Clicks the CTA button
- Asserts navigation to
/searchroute- Verifies the presence of the username input field
This aligns with the broader PR's shift from modal-based to route-based navigation.
app/user/page.tsx (2)
34-51: LGTM! Proper username normalization and encoding.The changes correctly:
- Trim whitespace from username for normalization
- Use normalized value consistently in metadata
- Apply
encodeURIComponentfor URL safety in canonical link
75-75: LGTM! Component replacement aligns with new user flow.Replacing
UserPageClientwithUserPageEditormatches the PR's objective of implementing a new user flow with enhanced editing capabilities.app/globals.css (2)
1-1: LGTM! KaTeX CSS imported for math rendering support.The KaTeX import correctly supports the new
MathTooltipContentcomponent and aligns with thekatexdependency added inpackage.json.
187-244: LGTM! Scrollbar utilities for consistent UI.The
.show-scrollbarand.overlay-scrollbarutilities provide consistent, theme-aware scrollbar styling. The approach using CSS variables ensures proper integration with the existing theming system.lib/api-utils.ts (2)
618-652: LGTM! Disabled card validation correctly skips unnecessary fields.The logic appropriately reduces validation requirements for disabled cards:
- Only
cardNameis validated as a required string- Color validation (both preset and explicit color fields) is correctly skipped
- Card type validation is still performed to ensure data integrity
This optimization aligns with the broader global/per-card settings architecture introduced in this PR.
674-674: LGTM! Disabled field correctly added to optional boolean fields.The
disabledfield is appropriately validated as an optional boolean, ensuring type safety for per-card configuration.app/examples/page.tsx (3)
4-4: LGTM! Navigation-based flow replaces modal-based generator.The router-based navigation to
/searchis clean and aligns with the broader refactor away from modal-based workflows. The callback is properly memoized withuseRouterdependency.Also applies to: 489-493
629-629: LGTM! ErrorBoundary reset keys simplified.The
resetKeyscorrectly depends only onsearchQuerynow that the modal-based generator state has been removed.
657-657: LGTM! Prop name changes are consistent across components.The prop rename from
onOpenGeneratortoonStartCreatingaccurately reflects the new navigation-based flow and is consistent with updates inCategorySectionandCTASectioncomponents.Also applies to: 667-667
tests/e2e/home/home.spec.ts (2)
4-14: LGTM! E2E test correctly validates navigation flow.The test properly verifies:
- Navigation to
/searchoccurs after CTA click- The search input is visible on the target page
This aligns with the shift away from modal-based interactions to direct navigation.
38-47: LGTM! Improved viewport visibility assertion.The updated viewport check is more robust by verifying both
topandbottompositions, ensuring the showcase is actually visible in the viewport rather than just having a visible top coordinate.package.json (1)
18-18: LGTM! Dependencies correctly added for new features.The new dependencies support the introduced features:
- Radix UI components for alert dialogs and tabs
- KaTeX for LaTeX math rendering
- TypeScript definitions for type safety
Version ranges are appropriate using caret notation for patch compatibility.
Also applies to: 27-27, 38-38, 57-57
components/examples/CTASection.tsx (1)
8-8: LGTM! Prop rename is complete and consistent.The
onOpenGenerator→onStartCreatingrename is consistently applied throughout the component and aligns with the navigation-based flow changes inapp/examples/page.tsx.Also applies to: 11-11, 42-42
components/MathTooltipContent.tsx (2)
48-77: LGTM! LaTeX processing logic and error handling are sound.The regex patterns correctly handle:
- Display math
$$...$$processed first to avoid conflicts- Inline math
$...$processed second, excluding newlines- Error handling gracefully falls back to original delimiters
The
containsMathhelper efficiently detects LaTeX presence.Also applies to: 85-87
25-77: The XSS concern does not apply here. All tooltip content comes from hardcoded values inCARD_INFO_TOOLTIPS(lib/card-info-tooltips.ts), which are application-generated strings containing only plain text and LaTeX math formulas—no user input or external HTML is involved.The example
<script>alert('xss')</script>cannot occur because the tooltip sources (countryDiversity,genreDiversity, etc.) are defined as static strings in the source code usingString.raw, with no HTML tags. KaTeX'srenderToString()safely renders math expressions, and content outside delimiters remains unchanged plain text. No sanitization is required when the content is trusted and controlled.If the optional
infoTooltipprop is used to override defaults, the responsibility for trusted content moves to the caller—the component should document this expectation if necessary.Likely an incorrect or invalid review comment.
tests/unit/api/card/card.test.ts (2)
1538-1570: LGTM! Test correctly validates DB lookup optimization.The test verifies that when:
colorPreset=custom- All required color params are in the URL
The route skips the cards DB lookup and uses only the user record, correctly passing URL-provided colors to the template. This validates an important performance optimization.
1572-1625: LGTM! Test validates URL color precedence with required DB config.The test correctly verifies that when:
- DB config is required for card-specific flags (e.g.,
showFavoritesforanimeStaff)- URL provides
colorPreset=customwith full color paramsURL-provided colors override DB-configured colors, even though the DB lookup still occurs for other fields. This validates the proper precedence rules in the color resolution logic.
app/page.tsx (1)
20-26: LGTM! Clean router-based navigation implementation.The transition from modal-based flow to router navigation is well-implemented. The
useCallbackhook correctly includesrouterin its dependency array, and the analytics tracking is preserved.components/examples/ExampleCard.tsx (1)
16-29: LGTM! Consistent prop rename.The prop rename from
onOpenGeneratortoonStartCreatingis cleanly applied across the interface definition and component destructuring. The button text update to "Create Your Cards" better reflects the new user flow.components/examples/CategorySection.tsx (1)
37-42: LGTM! Proper prop propagation.The
onStartCreatingprop rename is consistently applied through the interface, component signature, and child component invocation. The propagation chain aligns with theExampleCardAPI changes.Also applies to: 111-116, 184-192
components/user/ColorPreviewCard.tsx (1)
149-157:dangerouslySetInnerHTMLusage is acceptable here.The static analysis flagged this as a potential XSS vector. However, the SVG content is:
- Generated internally by
animeStatsTemplatewith controlled inputs- Processed through
stripTrustedSvgMarkerwhich implies a trust boundarySince this is not rendering user-provided HTML, the XSS risk is mitigated. Ensure that
animeStatsTemplateproperly sanitizes any user-controlled values (likeusername) if they're ever used with actual user input.components/user/GlobalSettingsPanel.tsx (1)
75-113: LGTM! Well-structured global settings panel.The component properly integrates with the Zustand store, uses appropriate memoization for handlers, and correctly wires up the
CardSettingsPanelin global mode.components/user/CardSettingsPanel.tsx (1)
55-94: LGTM! Clean discriminated union pattern.The type narrowing via
props.mode === "global"is idiomatic TypeScript, and the conditional rendering logic correctly handles all three states: global settings, card with custom settings enabled, and card with global settings fallback.components/user/BulkActionsToolbar.tsx (2)
100-159: Well-structured URL building with proper memoization.The
selectedCardscomputation correctly integrates with the store's effective color/border getters and properly maps configurations to URL parameters. The dependency array appears complete, and the null-filtering type guard is correctly implemented.
65-71: Good SSR-safe portal pattern.The
isMountedstate pattern correctly prevents hydration mismatches by deferring portal rendering until after client-side hydration. The early returns are properly ordered.Also applies to: 224-227
components/user/SettingsContent.tsx (4)
1-99: Well-structured component with clear prop interface.The component's props interface is comprehensive and well-documented with JSDoc comments. The separation of color, border, and advanced settings concerns is clean.
127-141: Visibility defaults and tab logic are correct.The default visibility calculation based on mode and the
hasAdvancedOptionscheck ensure the Advanced tab only appears when relevant options exist.
143-171: useMemo dependencies are complete.The memoization correctly includes all dependencies used in the callback:
idPrefix,mode,colors, andonColorChange.
398-434: Grid input handlers correctly clamp values.The inline clamping logic
Math.max(1, Math.min(5, Number(e.target.value) || 1))ensures grid dimensions stay within valid bounds (1-5). The fallback to1when parsing fails is appropriate.tests/e2e/user/user.spec.ts (3)
17-28: Error state expectations updated correctly.The updated assertions align with the new UI text for error states. Using regex patterns (
/something went wrong/i,/search for user/i) provides flexibility for minor text variations.
78-107: Bulk actions test is comprehensive.The test properly:
- Selects specific cards via checkboxes
- Verifies toolbar visibility with
data-testid- Checks button order and labels
- Tests popover interactions with appropriate timeouts
Good coverage of the new bulk actions workflow.
137-206: New user onboarding test provides good coverage.The test correctly mocks the full API sequence for a new user:
- 404 from
get-usertriggers Anilist lookup- Successful
get-anilistresponse- Store endpoints return success
get-cardsreturns empty array- Card endpoint returns valid SVG
The assertion that at least one preview image appears validates the initial cards snapshot behavior.
app/api/store-cards/route.ts (4)
52-69: Good defensive backfill for supported card types.
ensureAllSupportedCardTypesPresentensures Redis records always contain all supported card types, preventing partial records. Cards not explicitly provided are marked as disabled.
166-186: Variation fallback preserves pie settings when disabling cards.The logic correctly falls back to
previous?.variationwhenincoming.variationis undefined, ensuringshowPiePercentagessettings aren't lost when toggling a card's disabled state without re-specifying variation.
267-320: buildCardConfig correctly respects useCustomSettings.When
useCustomSettingsis false, color fields andborderColorare set toundefined, ensuring the card defers to global settings. The default oftruemaintains backward compatibility.
455-467: Global settings merge logic correctly handles conditional fields.The merged global settings object properly:
- Preserves
existingGlobalSettingswhen no newglobalSettingsprovided- Clears
borderColorandborderRadiuswhen borders disabled- Merges advanced settings with proper precedence
lib/card-data/config.ts (5)
13-25: Clean helper for validating complete color params.
hasAllRequiredColorParamsprovides a clear, reusable check for URL color completeness. The truthy check (!!params.titleColorParam) correctly handles bothnullandundefined.
48-56: Updated color resolution logic is correct.The comment and implementation correctly document that
colorPreset=customwithout individual colors requires DB fetch, whilecolorPreset=customwith all colors can be resolved from URL alone.
194-203: Custom preset URL override enhancement is well-implemented.Allowing URL color params to override when the preset is "custom" AND all required params are present enables sharable custom color links without DB dependency. The early return after
applyUrlColorParamsis correct.
436-440: Global settings merge placement is correct.Merging global settings into the card config before applying URL overrides ensures:
- Cards without per-card settings inherit from globals
- URL params can still override inherited values
559-609: mergeGlobalSettingsIntoConfig uses correct non-clobbering pattern.The
setIfMissinghelper elegantly ensures global settings only fill gaps without overwriting explicit per-card values. The conditional border application based onborderEnabledis appropriate.components/user/CardCategorySection.tsx (3)
50-63: Controlled/uncontrolled pattern is well-implemented.The component correctly:
- Determines mode via
typeof expanded === "boolean"- Only updates internal state when uncontrolled
- Always calls
onExpandedChangewhen providedThis follows React's controlled component conventions.
75-84: Good accessibility implementation.The header button properly uses:
type="button"to prevent form submissionaria-expandedfor screen readersaria-controlslinked to content viauseId
142-159: AnimatePresence correctly wraps conditional content.Unlike the issue in SettingsContent.tsx, this component properly wraps the conditional
motion.divwithAnimatePresence, enabling exit animations when the section collapses.lib/stores/index.ts (2)
19-32: New user-page-editor exports provide comprehensive API.The new exports include:
- Hook:
useUserPageEditor- Selectors:
selectEnabledCardIds,selectHasEnabledCards,selectCardConfigsByGroup- Types:
CardColorOverride,CardAdvancedSettings,CardEditorConfig, etc.This consolidates card editing state management into a single store.
6-7: Types are not removed, only excluded from barrel export.
UserPreferencesState,UserPreferencesActions, andUserPreferencesStoreremain exported fromlib/stores/user-preferences.ts. They are simply not re-exported in the barrel atlib/stores/index.ts. This is not a breaking change—no external consumers depend on these types.Likely an incorrect or invalid review comment.
components/ui/AlertDialog.tsx (2)
1-66: Standard Radix AlertDialog wrapper implementation.The component follows established shadcn/ui patterns:
- ForwardRef for components that need ref access
- DisplayName assignments for debugging
- Composition of Portal and Overlay in Content
- Consistent animation classes
137-171: Action and Cancel buttons correctly styled.
AlertDialogActionuses default button styles whileAlertDialogCanceluses outline variant with responsive margin (mt-2 sm:mt-0). This provides appropriate visual hierarchy for confirmation dialogs.tests/unit/api/store-cards/store-cards.test.ts (6)
157-172: Good validation test for disabled field type.Tests that non-boolean
disabledvalues (like"yes") are properly rejected with 400 status. This ensures type safety at the API boundary.
267-290: Storage assertions updated for backfill behavior.The test correctly verifies that:
- Total card count matches
displayNameslength (backfilled)- All supported card types are present
- Explicitly provided cards retain their configuration
Good validation of the
ensureAllSupportedCardTypesPresentbehavior.
322-384: Preserving settings when disabling is well-tested.This test validates a critical user experience scenario: toggling a card's disabled state shouldn't lose custom configurations. The mock setup and assertions are comprehensive.
946-1007: Border settings tests cover global/per-card interactions.Good coverage of:
borderColornot saved whenborderEnabled=false- Clearing existing
borderColorwhen disabling- Preserving per-card borders when global is disabled
1241-1273: Test verifies borderRadius clamping during merge.The test confirms that out-of-range values (150) are clamped to the maximum (100) when merging existing global settings. This validates the
clampBorderRadiusintegration.
1353-1384: Grid dimension clamping tests are thorough.Tests both:
- Clamping existing out-of-range values (999 → 5, -10 → 1)
- Clamping incoming values
Validates the
clampGridDimfunction integration for grid settings.components/user/CardTile.tsx (3)
97-142: LGTM - Well-structured component initialization.Good use of Zustand store selectors and fallback config object with sensible defaults. The memoized effective colors/border calculations will help prevent unnecessary re-renders.
156-193: LGTM - Preview URL construction is well-memoized.The dependency array correctly captures all values that affect the URL. The early return for missing userId prevents unnecessary computation.
232-252: LGTM - Download handler with proper error handling.Good use of loading state to prevent concurrent downloads, and proper cleanup in finally block.
lib/types/records.ts (3)
1-2: LGTM - Proper type import for ColorValue.The import enables gradient support for color fields throughout the records types.
444-462: LGTM - StoredCardConfig updates are well-designed.The changes properly support:
- Optional variation for backwards compatibility
- ColorValue type for gradient support
useCustomSettingsflag for explicit per-card override detectiondisabledflag for card visibility state
464-486: LGTM - GlobalCardSettings interface properly encapsulates shared configuration.The interface mirrors per-card settings appropriately, with
borderEnabledas the key differentiator for global border state. Integration with CardsRecord via optionalglobalSettingsfield maintains backwards compatibility.lib/card-groups.ts (3)
9-17: LGTM - colorToString correctly serializes ColorValue types.The function properly handles both string colors and gradient objects (serialized as JSON). The undefined check prevents unnecessary processing.
458-478: LGTM - Type signature properly separates color fields for ColorValue support.The
Omitpattern with re-declared color fields typed asColorValueallows the function to accept both string and gradient inputs while the originalStoredCardConfigmay have different typing. The newallowPresetColorOverridesoption provides flexibility for different use cases.
492-522: LGTM - Border and color inclusion logic is well-designed.Key behaviors:
borderRadiusis only included whenborderColoris present (sensible - radius without border is meaningless)- Colors are only included when
shouldIncludeColorsis true, respecting preset semantics- The
hasNamedPresetcheck prevents color overrides when using named presets (unless explicitly allowed)lib/stores/user-page-editor.ts (3)
358-445: LGTM - Robust server-to-editor config transformation.The function handles:
- Explicit
useCustomSettingsflag from server (preferred)- Legacy heuristic detection via color comparison for backwards compatibility
- Proper normalization of ColorValue for comparison (string vs JSON)
- Disabled state mapping to enabled flag
The logic correctly prioritizes explicit server flags over heuristics.
599-1110: LGTM - Comprehensive Zustand store implementation.The store is well-organized with:
- Clear action groupings (user data, global settings, card config, UI, bulk ops, save)
- Proper use of
devtoolsmiddleware for debugging- Consistent dirty state tracking on mutations
- Effective color/border resolution methods that handle inheritance
1148-1161: selectCardConfigsByGroup is incomplete and unused—the function is dead code.CardEditorConfig doesn't store group information, so this selector cannot group by statCardTypes without additional context. The function is exported but never referenced in the codebase. Either remove it or complete it by passing
statCardTypesas a parameter and implementing the proper grouping logic using thegroupproperty from each card type.components/stat-card-generator/constants.ts (3)
111-139: LGTM - Label simplifications improve readability.The removal of verbose qualifiers like "(Top 5 Count)" and "(Pie/Donut/Bar Breakdowns)" makes the UI cleaner while the card functionality remains unchanged.
141-179: LGTM - Consistent label simplification across manga and activity cards.Labels are now concise and consistent with the anime breakdown cards pattern.
243-267: LGTM - Core stats labels simplified."Anime Statistics" → "Anime Stats" etc. provides a cleaner UI without losing clarity.
components/user/UserPageEditor.tsx (6)
136-180: LGTM - Robust AniList user ID fetching with proper error handling.Good handling of specific HTTP status codes (404, 429) with user-friendly messages. The try/catch properly handles network failures.
257-264: LGTM - Initial cards snapshot provides sensible defaults.New users start with all cards enabled using the first variation and default preset, ensuring a complete initial experience.
384-496: LGTM - Comprehensive new user setup flow.The orchestration properly:
- Resolves userId from username if needed
- Fetches stats with proper error tracking
- Extracts username from stats as fallback
- Saves user to database
- Saves initial cards (non-fatal on failure)
- Extracts avatar URL
Error tracking is thorough with appropriate categories.
652-847: LGTM - Data loading effect with StrictMode protection.The
lastLoadedUserRefpattern correctly prevents duplicate API calls during React StrictMode double-mounting. The ref is cleared on errors to allow retries.Key behaviors:
- Validates userId/username before proceeding
- Handles 404 as "new user" trigger
- Falls back to initial snapshot if persisted cards fetch fails
- Proper cleanup and retry capability
728-780: Good fallback handling for missing persisted cards.The code handles the edge case where the cards we just saved aren't immediately available (eventual consistency). Falling back to
buildInitialCardsSnapshot()ensures the UI is never left in an inconsistent state.The warning log and error tracking provide visibility into potential storage issues.
1043-1541: LGTM - Well-structured responsive UI.The main render includes:
- Accessible toolbar with proper ARIA roles
- Responsive filter controls (mobile popover vs desktop inline)
- Confirmation dialog for destructive reset action
- Motion animations for smooth transitions
- Proper empty state handling
Good use of
cn()for conditional styling and consistent dark mode support.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/api-utils.ts (1)
619-653: Validation gap: disabled cards can store unvalidated color data.The current logic skips color validation for disabled cards (line 639-653), which means a disabled card could be stored with malformed color values:
{ "cardName": "animeStats", "disabled": true, "titleColor": "invalid-color-format" }This payload would pass validation but store invalid color data. When the card is later enabled, the invalid colors would be used.
Recommendation: Consider validating color fields even for disabled cards, or explicitly strip/ignore color fields when
disabled: trueto prevent storing invalid data.Suggested validation improvement
// Only validate color requirements when not disabled (disabled cards store minimal data) if (!isDisabled) { const rawPreset = card["colorPreset"]; const preset = typeof rawPreset === "string" && rawPreset.trim().length > 0 ? rawPreset : undefined; const requireColorFields = preset === undefined || preset === "custom"; if (requireColorFields) { const reqColorErr = validateRequiredColorFields( card, requiredColorFields, ); if (reqColorErr) return reqColorErr; } + } else { + // For disabled cards, validate that color fields are absent or valid if present + for (const field of requiredColorFields) { + const value = card[field]; + if (value !== undefined && value !== null && !validateColorValue(value)) { + const reason = getColorInvalidReason(value); + console.warn( + `⚠️ [${endpoint}] Disabled card ${cardIndex} has invalid color format for ${field} (${reason})`, + ); + return NextResponse.json( + { error: "Invalid data" }, + { status: 400, headers: apiJsonHeaders(request) }, + ); + } + } }
🧹 Nitpick comments (2)
components/user/CardTile.tsx (1)
136-142: Consider moving config defaults to the store layer.The component creates a default config object when the store doesn't have one. While this works, it places default-value responsibility in the component rather than the store. If defaults need to change or other components need the same defaults, you'll have duplication.
Consider having the store's selector or a getter method return a default config structure, keeping the source of truth centralized.
app/api/store-cards/route.ts (1)
351-391: Redundant validation: disabled field checked twice.The
validateDisabledBooleanFieldfunction (lines 356-391) validates that thedisabledfield is a boolean, but this validation is duplicated invalidateCardOptionalFields(lib/api-utils.ts, lines 674-692) which is called later byvalidateCardData.Validation flow:
- Line 416-422:
validateDisabledBooleanFieldchecks disabled is boolean- Line 425:
validateCardDatais called- Inside
validateCardData:validateCardOptionalFieldschecks disabled is boolean againRecommendation: Remove
validateDisabledBooleanFieldsincevalidateCardDataalready performs this validation. If early validation is desired for better error messages, document the rationale in a comment.Suggested removal
- /** - * Validates that every provided `disabled` field (if present) is a boolean. - * Returns a 400 response via `jsonWithCors` when an invalid value is found and - * increments analytics to track failed requests. - */ - async function validateDisabledBooleanField( - incomingCards: unknown, - endpoint: string, - endpointKey: string, - request?: Request, - ): Promise<NextResponse | null> { - if (!Array.isArray(incomingCards)) return null; - - for (let i = 0; i < incomingCards.length; i++) { - const card = incomingCards[i]; - - // Skip non-object entries -- they will be rejected by validateCardData later. - if (typeof card !== "object" || card === null) continue; - - const cardObj = card as Record<string, unknown>; - if ( - Object.prototype.hasOwnProperty.call(cardObj, "disabled") && - typeof cardObj.disabled !== "boolean" - ) { - console.warn( - `⚠️ [${endpoint}] Card ${i} has non-boolean disabled field: ${String( - cardObj.disabled, - )}`, - ); - await incrementAnalytics( - buildAnalyticsMetricKey(endpointKey, "failed_requests"), - ); - return jsonWithCors( - { error: "Invalid 'disabled' field type" }, - request, - 400, - ); - } - } - return null; - }And remove the call at lines 416-422:
- const disabledError = await validateDisabledBooleanField( - incomingCards, - endpoint, - endpointKey, - request, - ); - if (disabledError) return disabledError; - // Validate incoming data and obtain typed cards on success const validated = validateCardData(
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
app/api/store-cards/route.tscomponents/stat-card-generator/ColorPickerGroup.tsxcomponents/user/CardTile.tsxlib/api-utils.tsopenapi.yamltests/unit/api/store-cards/store-cards.test.ts
🧰 Additional context used
🧬 Code graph analysis (4)
app/api/store-cards/route.ts (4)
lib/card-data/validation.ts (1)
displayNames(79-134)lib/types/records.ts (3)
StoredCardConfig(445-462)GlobalCardSettings(465-479)CardsRecord(482-487)lib/utils.ts (2)
safeParse(769-802)clampBorderRadius(870-877)lib/api-utils.ts (2)
incrementAnalytics(364-371)buildAnalyticsMetricKey(379-387)
tests/unit/api/store-cards/store-cards.test.ts (2)
app/api/store-cards/route.ts (1)
POST(399-548)lib/card-data/validation.ts (1)
displayNames(79-134)
components/stat-card-generator/ColorPickerGroup.tsx (1)
lib/types/card.ts (1)
GradientStop(5-14)
lib/api-utils.ts (1)
lib/types/records.ts (1)
StoredCardConfig(445-462)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: E2E Mobile Chrome
- GitHub Check: E2E Chromium
🔇 Additional comments (17)
components/stat-card-generator/ColorPickerGroup.tsx (5)
238-240: LGTM!The initial state correctly ensures each stop has an ID by mapping over the incoming
stopsprop and falling back togenerateStopId()when theidfield is absent.
266-275: LGTM!The
handleStopChangecallback correctly operates onlocalStops, updates internal state, and propagates changes viaonStopsChange. The dependency array is appropriate.
277-289: LGTM!The
addStopfunction correctly:
- Guards against exceeding 5 stops using
localStops.length- Generates a unique ID for the new stop
- Updates both local state and propagates via callback
291-299: LGTM!The
removeStopfunction properly guards against removing stops when only 2 remain, operates onlocalStops, and propagates changes correctly.
340-341: LGTM!The rendering logic correctly derives disabled states and iterates over
localStops. The ARIA label dynamically reflects the current stop count.Also applies to: 352-352, 435-435
components/user/CardTile.tsx (1)
210-228: LGTM! Clipboard error handling properly implemented.The try-catch block correctly handles potential clipboard API failures. Error logging provides debugging visibility while the UI gracefully handles failures. This addresses the concern from the previous review.
openapi.yaml (1)
670-672: LGTM! Clean schema addition for disabled cards.The
disabledfield is well-documented and appropriately optional, allowing cards to be marked as hidden/disabled by users without requiring changes to existing stored configurations.lib/api-utils.ts (3)
12-12: Good addition for type safety.Importing
StoredCardConfigenables the improved return type forvalidateCardData, addressing the past review concern about type assertions bypassing static validation.
674-692: Correct validation for the disabled field.The disabled field is properly validated as an optional boolean alongside other optional boolean fields like
showFavorites,useStatusColors, andshowPiePercentages.
1008-1092: Excellent improvement addressing past review feedback!The updated
validateCardDatasignature now returnsStoredCardConfig[]on success instead ofnull, eliminating the type assertion issue flagged in the previous review (see past review comment at lines 410-411 in app/api/store-cards/route.ts).Benefits:
- Callers receive typed data directly without unsafe assertions
- Static type checking now covers the validated payload
- The typed construction (lines 1046-1089) properly handles all fields including the new
disabledanduseCustomSettingspropertiesThe implementation comprehensively maps all fields with appropriate type coercion (
coerceNumhelper for numeric fields, explicit boolean checks, and preservation of ColorValue types for color fields).app/api/store-cards/route.ts (6)
1-19: LGTM! Imports properly support new global settings model.The updated imports bring in necessary types (
GlobalCardSettings,StoredCardConfig,CardsRecord) and utilities (displayNamesfor card type enumeration,isValidCardTypefor validation) to support the enhanced per-card and global settings architecture.
52-69: Good backfilling strategy for data completeness.This function ensures that every supported card type exists in storage, adding missing types with
disabled: true. This prevents partial records and simplifies downstream logic that expects all card types to be present.The approach aligns with test expectations (e.g., test at lines 407-422 verifies backfilling behavior).
126-255: Well-structured helper functions for settings merging.These helpers provide clean separation of concerns:
parseExistingGlobalSettings: Safe extraction with fallback to undefined on parse errorscomputeShowPiePercentages(lines 166-186): Correctly considers both incoming and previous variation to determine if pie settings should be saved, addressing the edge case where disabling a card might omit variationcomputeShowFavorites: Properly checks card capability before mergingclampGridDim: Robust clamping with finite number checksmergeGlobalAdvancedSettings: Coherent merge strategy that prefers incoming values and applies clampingThe logic correctly handles all edge cases including undefined values and out-of-range inputs.
257-320: Complex but well-structured card config builder.
buildCardConfighandles intricate merging logic for per-card settings:
- useCustomSettings semantics (lines 274-282): Defaults to
true, determines whether to save color data at all- Color preset handling (lines 278-282): Individual colors only saved when
colorPresetis undefined or "custom"- Disabled card preservation (lines 295-296):
disabled: trueflag preserved, allowing settings to persist through disable/enable cycles- Border handling (lines 289-291): Per-card
borderColoronly saved whenuseCustomSettingsis true, independent of global border state- Advanced settings (lines 313-317): Properly computes favorites, status colors, and pie percentages using dedicated helpers
The logic correctly merges incoming and previous values while respecting the new per-card/global settings model.
322-349: Clean helper functions for card application and border radius computation.
- applyIncomingCards (lines 322-334): Straightforward iteration that applies incoming cards by merging with existing configs via
buildCardConfig- computeEffectiveBorderRadius (lines 336-349): Correctly computes border radius only when borders are enabled, with proper clamping and fallback to existing values
Both functions are simple, focused, and correctly implemented.
399-548: Comprehensive POST flow integrating global settings and typed validation.The updated POST handler successfully integrates:
- Global settings extraction (line 411): Accepts optional
globalSettingsin request body- Typed validation (lines 425-437): Uses updated
validateCardDatathat returns typed array, eliminating unsafe type assertions- Existing data merging (lines 460-476): Safely parses existing cards and global settings
- Card application (lines 483-486): Applies incoming cards and ensures all supported types exist
- Global settings computation (lines 488-519): Merges incoming and existing global settings with proper clamping and conditional logic
- Persistence (lines 521-530): Stores complete record with merged global settings
Key improvements:
- Line 431:
if (!Array.isArray(validated))correctly distinguishes validation failure (NextResponse) from success (typed array)- Lines 488-519: Complex but correct global settings merging with clamping for borderRadius and grid dimensions
- Line 524: globalSettings properly included in persisted record
The flow handles all edge cases including missing data, out-of-range values, and disabled cards.
tests/unit/api/store-cards/store-cards.test.ts (1)
1-1770: Excellent comprehensive test coverage for new features.The test suite thoroughly validates the enhanced global/per-card settings model:
Input Validation Coverage:
- Non-object card entries (lines 129-141)
- Non-boolean disabled fields (lines 171-193)
- Null cards arrays (line 122)
- Invalid JSON bodies (lines 155-169)
Disabled Cards Coverage:
- Acceptance and persistence (lines 314-341)
- Settings preservation when disabled (lines 343-405)
- Backfilling all supported types when empty (lines 407-422)
Color & Settings Coverage:
- Color preset handling (lines 847-911)
- useCustomSettings=false skips color persistence (lines 912-941)
- useCustomSettings=true preserves colors (lines 943-964)
Border Settings Coverage:
- Global borderEnabled interactions (lines 968-1055)
- Per-card border preservation when globally disabled (lines 1057-1137)
- Border radius clamping and merging (lines 1262-1294)
- useCustomSettings affects per-card borders (lines 1218-1260)
Global Advanced Settings Coverage:
- Persistence of useStatusColors, showPiePercentages, showFavorites, grid dims (lines 1298-1333)
- Preservation when incoming omits them (lines 1335-1372)
- Clamping of out-of-range grid dimensions (lines 1374-1427)
Assertions verify:
- Stored cards length matches
Object.keys(displayNames).length(lines 288, 332, 416, 474, 711, etc.)- Specific cards exist with expected properties
- Backfilling creates disabled entries for missing types
- Merging preserves previous values when incoming omits them
The tests provide strong confidence in correctness and edge-case handling.
|
@coderabbitai help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
CodeRabbit configuration file (
|
|
@coderabbitai configuration |
|
Configuration used: defaults CodeRabbit configurationlanguage: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
inheritance: false
reviews:
profile: chill
request_changes_workflow: false
high_level_summary: true
high_level_summary_instructions: ''
high_level_summary_placeholder: '@coderabbitai summary'
high_level_summary_in_walkthrough: false
auto_title_placeholder: '@coderabbitai'
auto_title_instructions: ''
review_status: true
commit_status: true
fail_commit_status: false
collapse_walkthrough: true
changed_files_summary: true
sequence_diagrams: true
estimate_code_review_effort: true
assess_linked_issues: true
related_issues: true
related_prs: true
suggested_labels: true
labeling_instructions: []
auto_apply_labels: false
suggested_reviewers: true
auto_assign_reviewers: false
in_progress_fortune: true
poem: true
enable_prompt_for_ai_agents: true
path_filters: []
path_instructions: []
abort_on_close: true
disable_cache: false
auto_review:
enabled: true
auto_incremental_review: true
ignore_title_keywords: []
labels: []
drafts: false
base_branches: []
ignore_usernames: []
finishing_touches:
docstrings:
enabled: true
unit_tests:
enabled: true
pre_merge_checks:
docstrings:
mode: warning
threshold: 80
title:
mode: warning
requirements: ''
description:
mode: warning
issue_assessment:
mode: warning
custom_checks: []
tools:
ast-grep:
rule_dirs: []
util_dirs: []
essential_rules: true
packages: []
shellcheck:
enabled: true
ruff:
enabled: true
markdownlint:
enabled: true
github-checks:
enabled: true
timeout_ms: 90000
languagetool:
enabled: true
enabled_rules: []
disabled_rules: []
enabled_categories: []
disabled_categories: []
enabled_only: false
level: default
biome:
enabled: true
hadolint:
enabled: true
swiftlint:
enabled: true
phpstan:
enabled: true
level: default
phpmd:
enabled: true
phpcs:
enabled: true
golangci-lint:
enabled: true
yamllint:
enabled: true
gitleaks:
enabled: true
checkov:
enabled: true
detekt:
enabled: true
eslint:
enabled: true
flake8:
enabled: true
fortitudeLint:
enabled: true
rubocop:
enabled: true
buf:
enabled: true
regal:
enabled: true
actionlint:
enabled: true
pmd:
enabled: true
clang:
enabled: true
cppcheck:
enabled: true
semgrep:
enabled: true
circleci:
enabled: true
clippy:
enabled: true
sqlfluff:
enabled: true
prismaLint:
enabled: true
pylint:
enabled: true
oxc:
enabled: true
shopifyThemeCheck:
enabled: true
luacheck:
enabled: true
brakeman:
enabled: true
dotenvLint:
enabled: true
htmlhint:
enabled: true
checkmake:
enabled: true
osvScanner:
enabled: true
chat:
art: true
auto_reply: true
integrations:
jira:
usage: auto
linear:
usage: auto
knowledge_base:
opt_out: false
web_search:
enabled: true
code_guidelines:
enabled: true
filePatterns: []
learnings:
scope: auto
issues:
scope: auto
jira:
usage: auto
project_keys: []
linear:
usage: auto
team_keys: []
pull_requests:
scope: auto
mcp:
usage: auto
disabled_servers: []
code_generation:
docstrings:
language: en-US
path_instructions: []
unit_tests:
path_instructions: []
issue_enrichment:
auto_enrich:
enabled: true
planning:
enabled: true
auto_planning:
enabled: true
labels: []
labeling:
labeling_instructions: []
auto_apply_labels: false
|
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
components/stat-card-generator/ColorPickerGroup.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
components/stat-card-generator/ColorPickerGroup.tsx (1)
lib/types/card.ts (1)
GradientStop(5-14)
🔇 Additional comments (2)
components/stat-card-generator/ColorPickerGroup.tsx (2)
261-294: LGTM! Callbacks correctly use localStops.All three callbacks (
handleStopChange,addStop,removeStop) correctly:
- Read from and update
localStopsstate- Include
localStopsin their dependency arrays- Propagate changes through
onStopsChangeThis ensures the local state stays in sync with user interactions.
335-347: LGTM! UI correctly renders from localStops.The component now renders based on
localStopsrather than thestopsprop, ensuring:
- Users see their changes immediately without waiting for parent re-render
- Disabled states (add/remove buttons) reflect the current local state
- ARIA labels provide accurate context
This is the correct pattern for controlled components with internal state management.
Also applies to: 430-430
|




Summary
This PR replaces the old generator-first workflow with a search-first user page editor. Instead of opening a generator modal, users now search for an AniList profile, land on a canonical
/user/[username]page, and manage their card setup directly in the editor.What changed
User page editor + new user flow
UserPageEditorand the supporting store/hooks/components for:Search, home, and examples flow
/searchthe main entry point into card creation/editing/searchinstead of opening the old generator/user/[username]while preserving lookup-based entry points through redirect logicPersistence, rendering, and API updates
globalSettingsifMatchUpdatedAtoptimistic concurrencyCleanup and removals
/settingsand/licensepages/StatCards/{username}/{key}.svgas a compatibility notice route instead of a live stat-card renderTests, docs, and infra