perf: implemented Unified Library, Analytics & Draft Management#369
perf: implemented Unified Library, Analytics & Draft Management#369bibhupradhanofficial wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive creator dashboard system with four new API routes for analytics, draft management, and NFT tracking, completely redesigns the dashboard UI with tabbed navigation and data-driven state management, and adds supporting infrastructure including a new Mongoose Draft model, utility function updates, and defensive code patterns across components. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User<br/>(Browser)
participant Dashboard as Dashboard<br/>Page
participant AnalyticsAPI as Analytics<br/>API
participant DraftsAPI as Drafts<br/>API
participant NFTAPI as NFTs<br/>API
participant Database as MongoDB
participant Blockchain as Blockchain<br/>(RPC)
User->>Dashboard: Load dashboard with wallet
par Fetch Analytics
Dashboard->>AnalyticsAPI: GET /api/creator/analytics?wallet=0x...
AnalyticsAPI->>Database: Find stories by wallet
Database-->>AnalyticsAPI: Story records
AnalyticsAPI->>Database: Aggregate UserInteractions (views/likes/bookmarks)
Database-->>AnalyticsAPI: Aggregated metrics
AnalyticsAPI-->>Dashboard: Totals + per-story metrics
and Fetch Drafts
Dashboard->>DraftsAPI: GET /api/creator/drafts?wallet=0x...
DraftsAPI->>Database: Query drafts with pagination
Database-->>DraftsAPI: Draft list with versions
DraftsAPI-->>Dashboard: Paginated drafts
and Fetch NFTs
Dashboard->>NFTAPI: GET /api/creator/nfts?wallet=0x...
NFTAPI->>Database: Find stories by author
NFTAPI->>Blockchain: Check transaction status (if pending)
Blockchain-->>NFTAPI: Tx receipt + status
NFTAPI-->>Dashboard: Stories with NFT sync status
end
Dashboard->>Dashboard: Merge cloud + local drafts
Dashboard->>Dashboard: Render tabbed interface
Dashboard-->>User: Display Analytics/Drafts/NFTs tabs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 5❌ Failed checks (5 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@app/api/creator/analytics/route.ts`:
- Around line 7-14: The GET handler currently only validates wallet format
(using VALIDATION_PATTERNS.walletAddress) but does not authenticate/authorize
the caller; update the handler to require and validate an authentication
token/session/signature (e.g., check a session cookie, JWT in Authorization
header, or verify a signed message) and ensure the authenticated identity
matches the requested wallet before returning analytics (otherwise return
401/403 via NextResponse); apply the same pattern to the handlers in
app/api/creator/drafts/route.ts, app/api/creator/drafts/[draftKey]/route.ts, and
app/api/creator/nfts/route.ts so each route (their GET functions) verifies auth
and owner equality with the wallet param.
- Around line 18-21: The query using
Story.find(...).select(...).sort(...).lean() fetches all stories for a wallet
into memory (variable stories) which can OOM for prolific creators; modify the
handler that calls Story.find to accept pagination parameters (e.g., page/limit
or cursor) and apply .limit(n).skip(page*limit) or a cursor-based filter to
Story.find, and update downstream aggregation logic that iterates over stories
to operate on a single page (or to stream/process incrementally) so analytics
run only on the bounded result set; ensure default limit is set (e.g., 50–100)
and expose pagination params in the API surface used by the dashboard.
In `@app/api/creator/nfts/route.ts`:
- Around line 36-60: The current implementation maps over candidates and calls
checkTxStatus for every pending tx concurrently (inside payload creation), which
can overwhelm RPC providers and also silently swallows errors; modify the flow
so pending tx checks are performed with bounded concurrency (e.g., process
candidates in chunks of ~5–10 or use a concurrency limiter when invoking
checkTxStatus) and avoid awaiting all RPC calls at once for payload generation,
cache or persist confirmed tokenId/status to skip re-checks, and replace the
empty catch with a processLogger.warn (or similar) that logs the txHash and the
caught error so RPC/connectivity issues are visible; key symbols to update:
candidates mapping that builds payload, the checkTxStatus calls, txHash,
tokenId, and syncStatus handling.
In `@app/dashboard/page.tsx`:
- Around line 308-328: openDraft currently writes draft metadata to localStorage
but does not include the revision counter, so after importCloudDraft calls
upsertDraftRecord and then openDraft the localDrafts memo can remain stale;
update openDraft (and the call site in importCloudDraft where upsertDraftRecord
is invoked) to include and increment the same revision counter used elsewhere
(the revision bump used in deleteDraft) when writing "storyCreationData" to
localStorage so localDrafts sees the change and the newly imported draft appears
without a full remount.
- Around line 330-336: The UI doesn't refresh because localDrafts (useMemo) only
depends on [draftPipelineFilter, draftQuery]; add a revision/trigger state
(e.g., draftRevision via useState) and include it in the dependency arrays for
localDrafts and draftCards so they recompute when changed, then increment that
revision inside deleteDraft immediately after calling
clearDraftRecord(draft.draftKey) to force a re-render of draft cards.
In `@models/Draft.ts`:
- Line 82: The schema options currently set { timestamps: true, strict: false }
on the Draft schema allow arbitrary fields to be persisted; update the schema
options used when creating DraftSchema (the Schema/Model definition in
models/Draft.ts) to remove strict: false or change it to a stricter policy
(e.g., omit strict to use default strict: true or set strict: 'throw') so
unknown fields are rejected; adjust any dependent code that relied on dynamic
fields if necessary.
🧹 Nitpick comments (8)
app/api/creator/nfts/route.ts (1)
3-3: Use path alias for consistency.All other imports in this file use the
@/path alias, butStoryuses a fragile relative path.Proposed fix
-import Story from '../../../../models/Story'; +import Story from '@/models/Story';models/Draft.ts (1)
69-69: Redundantindex: truealongsideunique: true.
unique: truealready creates a unique index ondraftKey, so the explicitindex: trueis unnecessary.Proposed fix
- draftKey: { type: String, required: true, unique: true, trim: true, index: true }, + draftKey: { type: String, required: true, unique: true, trim: true },app/api/creator/drafts/route.ts (2)
30-38: Regex-based text search on multiple fields without indexes can be slow.The
$orwith$regexon five fields (Lines 32–37) will perform collection scans unless text indexes or compound indexes exist fordraftKey,current.title,current.genre,storyType, andstoryFormat. For small datasets this is acceptable, but consider adding a MongoDB text index if this endpoint will be hit frequently or the collection grows.
88-90: Consider logging the caught error before returning a generic 500.The
errorvariable is caught but never logged. In a serverless/Next.js environment, silent 500s make debugging difficult. Even aconsole.errorwould help surface issues in production logs.🔧 Proposed fix
} catch (error) { + console.error('[GET /api/creator/drafts]', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }app/api/creator/drafts/[draftKey]/route.ts (1)
29-31: Inconsistent fallback:Date.now()here vs.nullfor top-level timestamps.
currentUpdatedAtfalls back toDate.now()(Line 31) when the field is missing, butcreatedAtandupdatedAtat Lines 66–67 fall back tonull. The same inconsistency appears in the version mapping (Line 55). This can mislead clients into thinking a draft was just updated when the field simply doesn't exist.Consider using
nullconsistently:♻️ Proposed fix
const currentUpdatedAt = draft.current?.updatedAt ? new Date(draft.current.updatedAt).getTime() - : Date.now(); + : null;- updatedAt: version.updatedAt ? new Date(version.updatedAt).getTime() : Date.now(), + updatedAt: version.updatedAt ? new Date(version.updatedAt).getTime() : null,app/dashboard/page.tsx (3)
111-132: Double-filtering local drafts: query filter applied redundantly.
localDrafts(Line 111) already filters by bothdraftPipelineFilteranddraftQuery. ThendraftCards(Line 193) appliesmatchesQuerytolocalDraftsagain. The pipeline filter is not re-checked indraftCardsfor local drafts becauselocalDraftsalready excluded non-matching records. However, the query check is applied twice — once inlocalDraftsand again indraftCardsat Line 194.This is not a bug but is wasted computation. Consider either removing the filter from
localDrafts(letdraftCardshandle all filtering) or removing the redundantmatchesQuerycheck in the local-drafts loop ofdraftCards.Also applies to: 134-221
466-474: Error messages usetext-muted-foreground— may be too subtle for errors.Errors from analytics, NFTs, and cloud drafts are displayed in muted text, making them easy to miss. Consider using a more visible treatment (e.g.,
text-destructiveor a bordered alert component) so users understand something is wrong.♻️ Proposed change
- {analyticsError && ( - <div className="text-sm text-muted-foreground">{analyticsError}</div> - )} - {nftsError && <div className="text-sm text-muted-foreground">{nftsError}</div>} - {cloudDraftsError && <div className="text-sm text-muted-foreground">{cloudDraftsError}</div>} + {analyticsError && ( + <div className="text-sm text-destructive">{analyticsError}</div> + )} + {nftsError && <div className="text-sm text-destructive">{nftsError}</div>} + {cloudDraftsError && <div className="text-sm text-destructive">{cloudDraftsError}</div>}
476-476: Avoidas anycast — use the union type directly.♻️ Proposed fix
- <Tabs value={tab} onValueChange={(value) => setTab(value as any)}> + <Tabs value={tab} onValueChange={(value) => setTab(value as "drafts" | "nfts" | "analytics")}>
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const { searchParams } = new URL(request.url); | ||
| const wallet = (searchParams.get('wallet') || '').trim().toLowerCase(); | ||
|
|
||
| if (!wallet || !VALIDATION_PATTERNS.walletAddress.test(wallet)) { | ||
| return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
No authentication — any caller can query analytics for any wallet.
All four /api/creator/* routes validate the wallet format but never verify that the caller actually owns the wallet (no session/JWT/signature check). This means any user can fetch analytics, drafts, and NFTs for any wallet address. If this data is considered private, an authentication layer is needed.
This concern applies equally to app/api/creator/drafts/route.ts, app/api/creator/drafts/[draftKey]/route.ts, and app/api/creator/nfts/route.ts.
🤖 Prompt for AI Agents
In `@app/api/creator/analytics/route.ts` around lines 7 - 14, The GET handler
currently only validates wallet format (using VALIDATION_PATTERNS.walletAddress)
but does not authenticate/authorize the caller; update the handler to require
and validate an authentication token/session/signature (e.g., check a session
cookie, JWT in Authorization header, or verify a signed message) and ensure the
authenticated identity matches the requested wallet before returning analytics
(otherwise return 401/403 via NextResponse); apply the same pattern to the
handlers in app/api/creator/drafts/route.ts,
app/api/creator/drafts/[draftKey]/route.ts, and app/api/creator/nfts/route.ts so
each route (their GET functions) verifies auth and owner equality with the
wallet param.
| const stories = await Story.find({ authorWallet: wallet }) | ||
| .select({ _id: 1, title: 1, status: 1, updatedAt: 1, nftTokenId: 1, nftTxHash: 1 }) | ||
| .sort({ updatedAt: -1 }) | ||
| .lean(); |
There was a problem hiding this comment.
Unbounded query: all stories for a wallet are fetched without limit.
If a prolific creator has thousands of stories, this query returns them all into memory, and the subsequent aggregation scans all their interactions. Consider adding a reasonable upper bound or pagination if the analytics response is meant to be consumed incrementally by the dashboard.
🤖 Prompt for AI Agents
In `@app/api/creator/analytics/route.ts` around lines 18 - 21, The query using
Story.find(...).select(...).sort(...).lean() fetches all stories for a wallet
into memory (variable stories) which can OOM for prolific creators; modify the
handler that calls Story.find to accept pagination parameters (e.g., page/limit
or cursor) and apply .limit(n).skip(page*limit) or a cursor-based filter to
Story.find, and update downstream aggregation logic that iterates over stories
to operate on a single page (or to stream/process incrementally) so analytics
run only on the bounded result set; ensure default limit is set (e.g., 50–100)
and expose pagination params in the API surface used by the dashboard.
| const payload = await Promise.all(candidates.map(async (story: any) => { | ||
| const txHash = story.nftTxHash || undefined; | ||
| let tokenId = story.nftTokenId || undefined; | ||
| const status = story.status || 'draft'; | ||
| let syncStatus: 'missing' | 'pending' | 'confirmed' | 'failed' = | ||
| status === 'failed' | ||
| ? 'failed' | ||
| : tokenId | ||
| ? 'confirmed' | ||
| : txHash || status === 'publishing' | ||
| ? 'pending' | ||
| : 'missing'; | ||
|
|
||
| if (txHash && syncStatus === 'pending') { | ||
| try { | ||
| const chainStatus = await checkTxStatus(txHash); | ||
| if (chainStatus.status === 'confirmed') { | ||
| syncStatus = 'confirmed'; | ||
| tokenId = tokenId || (chainStatus as any).tokenId || undefined; | ||
| } else if (chainStatus.status === 'reverted') { | ||
| syncStatus = 'failed'; | ||
| } | ||
| } catch { | ||
| } | ||
| } |
There was a problem hiding this comment.
Unbounded Promise.all for on-chain status checks can overwhelm the RPC provider.
Every pending NFT triggers a concurrent checkTxStatus RPC call. For a creator with many pending NFTs, this could hit rate limits or cause timeouts. Consider batching or limiting concurrency (e.g., processing in chunks of 5–10), or caching confirmed statuses so they aren't re-checked.
Also, the empty catch block on lines 58–59 silently swallows RPC errors, making it hard to diagnose blockchain connectivity issues. Consider logging at warn level.
Sketch: limit concurrency and add logging
+ // Process in batches to avoid RPC rate limiting
+ const BATCH_SIZE = 5;
+ for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
+ const batch = candidates.slice(i, i + BATCH_SIZE);
+ await Promise.all(batch.map(async (story: any) => { /* ... */ }));
+ }
} catch {
+ console.warn(`[NFTs] Failed to check tx status for ${txHash}`);
}🤖 Prompt for AI Agents
In `@app/api/creator/nfts/route.ts` around lines 36 - 60, The current
implementation maps over candidates and calls checkTxStatus for every pending tx
concurrently (inside payload creation), which can overwhelm RPC providers and
also silently swallows errors; modify the flow so pending tx checks are
performed with bounded concurrency (e.g., process candidates in chunks of ~5–10
or use a concurrency limiter when invoking checkTxStatus) and avoid awaiting all
RPC calls at once for payload generation, cache or persist confirmed
tokenId/status to skip re-checks, and replace the empty catch with a
processLogger.warn (or similar) that logs the txHash and the caught error so
RPC/connectivity issues are visible; key symbols to update: candidates mapping
that builds payload, the checkTxStatus calls, txHash, tokenId, and syncStatus
handling.
| const openDraft = (draft: StoryDraftRecord) => { | ||
| try { | ||
| setActiveDraftKey(draft.draftKey); | ||
| localStorage.setItem( | ||
| "storyCreationData", | ||
| JSON.stringify({ | ||
| type: draft.storyType || "text", | ||
| format: draft.storyFormat || "free", | ||
| draftKey: draft.draftKey, | ||
| timestamp: Date.now(), | ||
| }) | ||
| ); | ||
| router.push("/create"); | ||
| } catch { | ||
| toast({ | ||
| title: "Unable to open draft", | ||
| description: "Please try again.", | ||
| variant: "destructive", | ||
| }); | ||
| } | ||
| }, []); | ||
| }; |
There was a problem hiding this comment.
openDraft also needs the revision counter for consistency.
After importCloudDraft calls upsertDraftRecord (Line 352) and then openDraft, the local draft store is updated. If the user navigates back to the dashboard without a full remount, the same stale-memo issue applies — the newly imported draft won't appear in localDrafts. The revision counter fix from the deleteDraft comment should also bump after upsertDraftRecord in importCloudDraft.
🤖 Prompt for AI Agents
In `@app/dashboard/page.tsx` around lines 308 - 328, openDraft currently writes
draft metadata to localStorage but does not include the revision counter, so
after importCloudDraft calls upsertDraftRecord and then openDraft the
localDrafts memo can remain stale; update openDraft (and the call site in
importCloudDraft where upsertDraftRecord is invoked) to include and increment
the same revision counter used elsewhere (the revision bump used in deleteDraft)
when writing "storyCreationData" to localStorage so localDrafts sees the change
and the newly imported draft appears without a full remount.
| const deleteDraft = (draft: StoryDraftRecord) => { | ||
| clearDraftRecord(draft.draftKey); | ||
| toast({ | ||
| title: "Draft deleted", | ||
| description: draft.current?.title?.trim() ? draft.current.title : "Untitled draft", | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Bug: UI does not update after deleting a local draft.
deleteDraft calls clearDraftRecord to remove the draft from localStorage, but localDrafts (useMemo at Line 111) only depends on [draftPipelineFilter, draftQuery]. Since neither dependency changes, the memo doesn't recompute, and the deleted draft card remains visible until the user changes a filter or navigates away.
A simple fix is to introduce a counter/trigger state that increments on delete and is included in the dependency arrays of both localDrafts and draftCards.
🐛 Proposed fix
Add a revision counter:
+ const [localDraftRevision, setLocalDraftRevision] = useState(0);Update the dependency arrays:
const localDrafts = useMemo(() => {
// ... existing code
- }, [draftPipelineFilter, draftQuery]);
+ }, [draftPipelineFilter, draftQuery, localDraftRevision]);- }, [cloudDrafts, draftPipelineFilter, draftQuery, localDrafts]);
+ }, [cloudDrafts, draftPipelineFilter, draftQuery, localDrafts, localDraftRevision]);Bump the counter on delete:
const deleteDraft = (draft: StoryDraftRecord) => {
clearDraftRecord(draft.draftKey);
+ setLocalDraftRevision((r) => r + 1);
toast({
title: "Draft deleted",
description: draft.current?.title?.trim() ? draft.current.title : "Untitled draft",
});
};🤖 Prompt for AI Agents
In `@app/dashboard/page.tsx` around lines 330 - 336, The UI doesn't refresh
because localDrafts (useMemo) only depends on [draftPipelineFilter, draftQuery];
add a revision/trigger state (e.g., draftRevision via useState) and include it
in the dependency arrays for localDrafts and draftCards so they recompute when
changed, then increment that revision inside deleteDraft immediately after
calling clearDraftRecord(draft.draftKey) to force a re-render of draft cards.
| lastEditedByAIAt: { type: Date, default: null }, | ||
| }, | ||
| }, | ||
| { timestamps: true, strict: false } |
There was a problem hiding this comment.
strict: false disables schema validation for unknown fields.
This allows arbitrary, unvalidated fields to be persisted to MongoDB. Unless there's a specific need for dynamic/ad-hoc fields, this is a data integrity risk — typos in field names, injected fields from unsanitized input, etc. will silently be stored.
Consider removing it (Mongoose defaults to strict: true) or switching to strict: 'throw' if you want to catch unexpected fields.
Proposed fix
- { timestamps: true, strict: false }
+ { timestamps: true }📝 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.
| { timestamps: true, strict: false } | |
| { timestamps: true } |
🤖 Prompt for AI Agents
In `@models/Draft.ts` at line 82, The schema options currently set { timestamps:
true, strict: false } on the Draft schema allow arbitrary fields to be
persisted; update the schema options used when creating DraftSchema (the
Schema/Model definition in models/Draft.ts) to remove strict: false or change it
to a stricter policy (e.g., omit strict to use default strict: true or set
strict: 'throw') so unknown fields are rejected; adjust any dependent code that
relied on dynamic fields if necessary.
|
Right now the PR description is basically the raw template, there’s no real summary of what you shipped or why. For something this big (new creator APIs, dashboard redesign, Draft model, NFT sync, etc.) that’s a blocker. Create a new PR focused on “Creator Dashboard 2.0 – Unified Library, Analytics & Draft Management” with:
Until that’s done, I can’t treat this as merge‑ready, even if the implementation looks promising. |
📝 Description
Issue Reference (e.g. Fixes #123): ---
Summary of Changes (high-level):
Context / Motivation:
🏗️ Type of Change
Select the relevant categories:
⚙️ Technical Checklist
Tick everything that applies. Leave non‑applicable items unchecked.
AI / Application Logic
GROQ_API_KEY.Web3 / Smart Contracts
smart_contractsand they passed.Frontend / UX / Accessibility
aria-*, alt text) and semantic HTML for new UI.Backend / Database
Security & Privacy
.envfiles are committed.Code Quality
npm run lint(or equivalent) and resolved reported issues.anyby default.🧪 Testing Evidence
Describe how you tested these changes. Include commands and logs where possible.
If this PR affects UI flows, also describe the manual test steps you followed.
📸 Visual Proof (for UI / UX changes)
👤 Contributor Status
Tick all that apply to you for this PR:
🔁 Review & Impact
Breaking Changes
Dependencies
Backward Compatibility / Migrations
✅ Final Acknowledgements (Mandatory or will be marked invalid)
You must check all of the following before requesting review. These are required:
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes