feat: add publishing graph to posted ui#54
Conversation
|
@mezotv is attempting to deploy a commit to the joschan21's projects Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds a ContributionGraph UI, a PostGraph client component to the Posted page, a new postedRouter with activity/stats/recent endpoints, and wires the router into the app router. PostGraph fetches yearly activity, renders a heatmap with loading/fallback fake data, tooltips, legend, and totals. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant P as Posted Page
participant G as PostGraph (Client)
participant Q as React Query
participant S as Server appRouter
participant R as postedRouter
U->>P: Navigate to /studio/posted
P->>G: Render <PostGraph/>
G->>Q: useQuery posted.get_publishing_activity{year, accountId}
Q->>S: tRPC request
S->>R: route to postedRouter.get_publishing_activity
R->>DB: Query published tweets by user/year (+optional account)
R-->>S: {activity[], totalCount, year, maxCount, maxLevel}
S-->>Q: Response
alt loading/error/empty
G->>G: Generate deterministic fakeData
G-->>U: Render heatmap (blur + overlay)
else real data
G-->>U: Render heatmap with tooltips, legend, totals
end
sequenceDiagram
autonumber
participant C as Client (tRPC)
participant R as postedRouter
participant DB as Database
rect rgba(220,240,255,0.35)
note over C,R: get_publishing_activity(year?, accountId?)
C->>R: Request
R->>DB: Aggregate published tweets per day for year (+optional account)
R->>R: Map counts -> levels (0–4), compute totals
R-->>C: {activities[], totalCount, year, maxCount, maxLevel}
end
rect rgba(220,255,220,0.28)
note over C,R: get_publishing_stats(accountId?)
C->>R: Request
R->>DB: Compute totalPublished, thisMonthPublished, scheduledCount
R-->>C: {totalPublished, thisMonthPublished, scheduledCount}
end
rect rgba(255,240,220,0.28)
note over C,R: get_recent_published(limit?, accountId?)
C->>R: Request
R->>DB: Fetch recent published tweets (limit)
R-->>C: [{id, content, createdAt, twitterId, accountId}...]
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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: 0
🧹 Nitpick comments (15)
src/components/ui/kibo-ui/contribution-graph/index.tsx (5)
161-166: Fix unsafe typing in padded weeks array.
paddedActivitiesis declared asActivity[]but actually containsundefinedelements. This is unsound and can hide bugs.Apply this diff:
- const paddedActivities = [ - ...(new Array(differenceInCalendarDays(firstDate, firstCalendarDate)).fill( - undefined - ) as Activity[]), - ...normalizedActivities, - ]; + const paddedActivities: Array<Activity | undefined> = [ + ...new Array(differenceInCalendarDays(firstDate, firstCalendarDate)).fill( + undefined + ), + ...normalizedActivities, + ];
156-160: Prefer startOfWeek for clarity and fewer edge cases.Computing the first calendar date via
nextDay(...)-subWeeks(1)is harder to read and more error‑prone thanstartOfWeek.Apply this diff (and add the import):
import { differenceInCalendarDays, eachDayOfInterval, formatISO, getDay, getMonth, getYear, - nextDay, + startOfWeek, parseISO, subWeeks, } from "date-fns"; @@ - const firstCalendarDate = - getDay(firstDate) === weekStart - ? firstDate - : subWeeks(nextDay(firstDate, weekStart), 1); + const firstCalendarDate = startOfWeek(firstDate, { weekStartsOn: weekStart });
184-189: Don’t throw on empty weeks; skip gracefully.An empty week can occur with unusual input. Throwing breaks rendering when a no‑data week slips in.
Apply this diff:
- if (!firstActivity) { - throw new Error( - `Unexpected error: Week ${weekIndex + 1} is empty: [${week}].` - ); - } + if (!firstActivity) { + return labels; + }
116-118: Minor: build the map from the sorted array or rename var.You sort into
sortedActivitiesbut build thecalendarmap fromactivities. Not wrong, just inconsistent.Apply this diff:
- const calendar = new Map<string, Activity>( - activities.map((a) => [a.date, a]) - ); + const calendar = new Map<string, Activity>( + sortedActivities.map((a) => [a.date, a]) + );
240-256: Expose server maxLevel to the graph (defensive).The server also computes
maxLevel. Even though both use 4 today, piping it through keeps client/server in sync if this changes later.You don’t need code changes here; see the suggested change in PostGraph to pass
maxLeveldown.src/app/studio/posted/page.tsx (1)
20-20: Optional: lazy‑load PostGraph to trim initial bundle.The graph is non‑critical; consider
next/dynamicwith a skeleton fallback.src/components/posted/post-graph.tsx (4)
66-71: Align ContributionGraph with server totals and maxLevel.Pass
totalCountandmaxLevelso the context and legend match server output; otherwise, future server changes could desync the legend or totals.Apply this diff:
- <ContributionGraph data={data} blockSize={10} blockMargin={3}> + <ContributionGraph + data={data} + blockSize={10} + blockMargin={3} + totalCount={totalCount} + maxLevel={shouldUseFakeData ? 4 : apiData?.maxLevel ?? 4} + >
97-101: Disable interactions when showing fake data.The graph is blurred but still interactive (tooltips fire). Add
pointer-events-nonewhenshouldUseFakeData.Apply this diff:
- <div className={cn(shouldUseFakeData && "blur-sm")}> + <div className={cn(shouldUseFakeData && "blur-sm pointer-events-none")}>
42-64: Make fake data deterministic per day/year.
Math.random()changes on every mount. Use a cheap hash from the date string for stable demo data (no new deps).Apply this diff:
- const fakeData = useMemo(() => { + const fakeData = useMemo(() => { const maxCount = 20; const maxLevel = 4; const now = new Date(); const days = eachDayOfInterval({ start: startOfYear(now), end: endOfYear(now), }); - return days.map((date) => { - const c = Math.round( - Math.random() * maxCount - Math.random() * (0.8 * maxCount) - ); + const hash = (s: string) => { + let h = 0; + for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + return Math.abs(h); + }; + + return days.map((date) => { + const iso = formatISO(date, { representation: "date" }); + const r = (hash(iso) % 1000) / 1000; // 0..1 stable per day + const c = Math.round(r * maxCount * 0.8); // bias lower a bit const count = Math.max(0, c); const level = Math.ceil((count / maxCount) * maxLevel); return { - date: formatISO(date, { representation: "date" }), + date: iso, count, level }; }); }, []);
101-117: Use the rect as the Tooltip trigger for better SVG event targeting.Wrapping
<g>works but some tooltip libs expect a focusable element; using the<rect>avoids group-level quirks.Apply this diff:
- <Tooltip> - <TooltipTrigger asChild> - <g> - <ContributionGraphBlock + <Tooltip> + <TooltipTrigger asChild> + <ContributionGraphBlock activity={activity} className={cn( 'data-[level="0"]:fill-muted dark:data-[level="0"]:fill-muted', 'data-[level="1"]:fill-primary/20 dark:data-[level="1"]:fill-primary/30', 'data-[level="2"]:fill-primary/40 dark:data-[level="2"]:fill-primary/50', 'data-[level="3"]:fill-primary/60 dark:data-[level="3"]:fill-primary/70', 'data-[level="4"]:fill-primary/80 dark:data-[level="4"]:fill-primary/90', )} dayIndex={dayIndex} weekIndex={weekIndex} /> - </g> </TooltipTrigger>src/server/routers/posted-router.ts (5)
36-44: Guardand(...)inputs explicitly; avoid passingundefined.Depending on Drizzle version,
and()may not ignoreundefined. Safer to filter falsy entries before composing the predicate.Apply this diff:
- .where( - and( - eq(tweets.userId, user.id), - eq(tweets.isPublished, true), - gte(tweets.createdAt, startDate), - sql`${tweets.createdAt} <= ${endDate}`, - accountId ? eq(tweets.accountId, accountId) : undefined - ) - ) + .where(and( + ...[ + eq(tweets.userId, user.id), + eq(tweets.isPublished, true), + gte(tweets.createdAt, startDate), + sql`${tweets.createdAt} <= ${endDate}`, + accountId ? eq(tweets.accountId, accountId) : undefined, + ].filter(Boolean) as any[] + ))
32-47: Index‑friendly date filtering.
DATE(created_at)prevents index use. Prefer range queries oncreated_atand group by the date cast only inSELECT/GROUP BYresult, not in theWHERE—which you already do. To improve grouping, also cast once with an alias and reuse it.Apply this diff to reuse the computed date:
- const dailyTweets = await db - .select({ - date: sql<string>`DATE(${tweets.createdAt})`.as('date'), - count: sql<number>`COUNT(*)`.as('count'), - }) + const dayExpr = sql<string>`DATE(${tweets.createdAt})`; + const dailyTweets = await db + .select({ + date: dayExpr.as('date'), + count: sql<number>`COUNT(*)`.as('count'), + }) .from(tweets) .where( and( eq(tweets.userId, user.id), eq(tweets.isPublished, true), gte(tweets.createdAt, startDate), sql`${tweets.createdAt} <= ${endDate}`, accountId ? eq(tweets.accountId, accountId) : undefined ) ) - .groupBy(sql`DATE(${tweets.createdAt})`) - .orderBy(sql`DATE(${tweets.createdAt})`) + .groupBy(dayExpr) + .orderBy(dayExpr)
53-63: Zero‑count level mapping: clamp once for readability.Current logic is fine; a small refactor reads cleaner.
Apply this diff:
- const level = count === 0 ? 0 : Math.min(Math.ceil((count / maxCount) * maxLevel), maxLevel) + const level = count === 0 ? 0 : Math.min(maxLevel, Math.ceil((count / maxCount) * maxLevel))
73-81: Return shape: consider echoingweekStartfor clients.Clients can align calendars without guessing. Optional, but helps keep UI/server aligned.
return c.superjson({ activity, totalCount, year, maxCount, maxLevel, + weekStart: 0, })
84-137: Stats queries: factor common predicates.The three stats queries repeat the same
and(...)pattern. Extract aconst whereBase = [...]and reuse to reduce mistakes.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/app/studio/posted/page.tsx(2 hunks)src/components/posted/post-graph.tsx(1 hunks)src/components/ui/kibo-ui/contribution-graph/index.tsx(1 hunks)src/server/index.ts(1 hunks)src/server/routers/posted-router.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx,py,go,java,rb}
📄 CodeRabbit inference engine (.cursor/rules/posthog-integration.mdc)
Never hallucinate an API key. Instead, always use the API key populated in the .env file.
Files:
src/server/index.tssrc/server/routers/posted-router.tssrc/app/studio/posted/page.tsxsrc/components/posted/post-graph.tsxsrc/components/ui/kibo-ui/contribution-graph/index.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/posthog-integration.mdc)
**/*.{ts,tsx,js,jsx}: If using TypeScript, use an enum to store flag names. If using JavaScript, store flag names as strings to an object declared as a constant, to simulate an enum. Use a consistent naming convention for this storage.
enum/const object members should be written UPPERCASE_WITH_UNDERSCORE.
Gate flag-dependent code on a check that verifies the flag's values are valid and expected.
If a custom property for a person or event is at any point referenced in two or more files or two or more callsites in the same file, use an enum or const object, as above in feature flags.
Files:
src/server/index.tssrc/server/routers/posted-router.tssrc/app/studio/posted/page.tsxsrc/components/posted/post-graph.tsxsrc/components/ui/kibo-ui/contribution-graph/index.tsx
🧬 Code graph analysis (3)
src/server/routers/posted-router.ts (3)
src/server/jstack.ts (2)
j(34-34)privateProcedure(52-52)src/db/schema/tweet.ts (1)
tweets(19-49)src/db/schema/auth.ts (1)
user(3-19)
src/components/posted/post-graph.tsx (4)
src/hooks/account-ctx.tsx (1)
useAccount(51-55)src/lib/client.ts (1)
client(8-11)src/lib/utils.ts (1)
cn(6-8)src/components/ui/kibo-ui/contribution-graph/index.tsx (6)
ContributionGraph(240-304)ContributionGraphCalendar(365-420)ContributionGraphBlock(312-350)ContributionGraphFooter(424-435)ContributionGraphTotalCount(444-464)ContributionGraphLegend(473-517)
src/components/ui/kibo-ui/contribution-graph/index.tsx (1)
src/lib/utils.ts (1)
cn(6-8)
🔇 Additional comments (3)
src/app/studio/posted/page.tsx (1)
3-3: LGTM: PostGraph import is correct.src/components/posted/post-graph.tsx (1)
30-40: Future‑proof: server input year bound is 2030.Your
currentYearcomes from the system clock; after 2030 the request will fail input validation. Consider clamping to server bounds or surfacing a clear error.Apply this diff (optional):
- const currentYear = new Date().getFullYear(); + const currentYear = Math.min(new Date().getFullYear(), 2030);src/server/index.ts (1)
45-47: Router wiring looks correct — regenerate client types & verify posted usage compilesFound client usage at src/components/posted/post-graph.tsx (lines 33–36: client.posted.getPublishingActivity.$get). Regenerate your tRPC client types (and run a full typecheck/build), then start the app to confirm the new posted routes compile and are reachable.
Adds a github like graph to the dashboard showing the tweet activity ^^
Screenshot
PLEASE TEST BEFORE MERGING!
Summary by CodeRabbit