Skip to content

Commit c3ac901

Browse files
vcarlclaude
andcommitted
Fix guild context hydration bug by moving DiscordLayout to auth layout
- Remove GuildContext provider and useGuilds hook that caused hydration issues - Move DiscordLayout component to __auth.tsx layout level - Pass guilds as direct props instead of through React context - Remove DiscordLayout wrappers from child route components - Simplify component architecture and eliminate context timing issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cf5d5c6 commit c3ac901

File tree

7 files changed

+404
-594
lines changed

7 files changed

+404
-594
lines changed

app/components/DiscordLayout.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@ import { useState } from "react";
22
import { Link, useLocation } from "react-router";
33
import { useUser } from "#~/utils";
44
import { Logout } from "#~/basics/logout";
5-
import { useGuilds } from "#~/routes/__auth";
65

76
interface DiscordLayoutProps {
87
children: React.ReactNode;
98
rightPanel?: React.ReactNode;
9+
guilds: Array<{
10+
id: string;
11+
name: string;
12+
icon?: string;
13+
hasBot: boolean;
14+
authz: string[];
15+
}>;
1016
}
1117

12-
export function DiscordLayout({ children, rightPanel }: DiscordLayoutProps) {
18+
export function DiscordLayout({
19+
children,
20+
rightPanel,
21+
guilds,
22+
}: DiscordLayoutProps) {
1323
const user = useUser();
1424
const location = useLocation();
1525
const [accountExpanded, setAccountExpanded] = useState(false);
16-
const { guilds } = useGuilds();
1726

1827
// Filter to only show manageable guilds (where Euno is installed) in the server selector
1928
const manageableGuilds = guilds.filter((guild) => guild.hasBot);
@@ -170,7 +179,7 @@ export function DiscordLayout({ children, rightPanel }: DiscordLayoutProps) {
170179
</div>
171180

172181
{/* Main Content Area */}
173-
<div className="flex flex-1 overflow-hidden">
182+
<div className="flex flex-1 overflow-hidden bg-gray-700">
174183
{/* Main Content */}
175184
<main className={`flex-1 overflow-auto ${rightPanel ? "pr-0" : ""}`}>
176185
<div className="h-full bg-gray-700">{children}</div>

app/routes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { route, layout } from "@react-router/dev/routes";
33

44
export default [
55
layout("routes/__auth.tsx", [
6-
route("guilds", "routes/guilds.tsx"),
76
route("app/:guildId/onboard", "routes/onboard.tsx"),
87
route("app/:guildId/sh", "routes/__auth/dashboard.tsx"),
98
route("app/:guildId/sh/:userId", "routes/__auth/sh-user.tsx"),

app/routes/__auth.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,24 @@ import { fetchGuilds } from "#~/models/discord.server";
77
import { rest } from "#~/discord/api.js";
88
import { REST } from "@discordjs/rest";
99
import { log, trackPerformance } from "#~/helpers/observability";
10-
import { createContext, useContext } from "react";
10+
import { DiscordLayout } from "#~/components/DiscordLayout";
1111
import TTLCache from "@isaacs/ttlcache";
1212

13-
// Guild context for sharing guild data across authenticated routes
14-
interface GuildContextType {
15-
guilds: Array<{
13+
// TTL cache for guild data - 5 minute TTL, max 100 users
14+
const guildCache = new TTLCache<
15+
string,
16+
Array<{
1617
id: string;
1718
name: string;
1819
icon?: string;
1920
hasBot: boolean;
2021
authz: string[];
21-
}>;
22-
}
23-
24-
// TTL cache for guild data - 5 minute TTL, max 100 users
25-
const guildCache = new TTLCache<string, GuildContextType["guilds"]>({
22+
}>
23+
>({
2624
ttl: 5 * 60 * 1000, // 5 minutes
2725
max: 100, // max 100 users cached
2826
});
2927

30-
const GuildContext = createContext<GuildContextType | null>(null);
31-
32-
export function useGuilds() {
33-
const context = useContext(GuildContext);
34-
if (!context) {
35-
throw new Error("useGuilds must be used within a GuildProvider");
36-
}
37-
return context;
38-
}
39-
4028
export async function loader({ request }: Route.LoaderArgs) {
4129
const user = await getUser(request);
4230

@@ -85,6 +73,13 @@ export default function Auth() {
8573
const { pathname, search, hash } = useLocation();
8674
const { guilds } = useLoaderData<typeof loader>();
8775

76+
console.log("🏠 Auth component rendering:", {
77+
hasUser: !!user,
78+
guildsCount: guilds?.length || 0,
79+
guilds,
80+
pathname,
81+
});
82+
8883
if (!user) {
8984
return (
9085
<div className="flex min-h-full flex-col justify-center">
@@ -96,8 +91,8 @@ export default function Auth() {
9691
}
9792

9893
return (
99-
<GuildContext.Provider value={{ guilds }}>
94+
<DiscordLayout guilds={guilds}>
10095
<Outlet />
101-
</GuildContext.Provider>
96+
</DiscordLayout>
10297
);
10398
}

app/routes/__auth/dashboard.tsx

Lines changed: 56 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Route } from "./+types/dashboard";
22
import { data, useSearchParams, Link } from "react-router";
33
import type { LabelHTMLAttributes, PropsWithChildren } from "react";
44
import { getTopParticipants } from "#~/models/activity.server";
5-
import { DiscordLayout } from "#~/components/DiscordLayout";
65

76
export async function loader({ params, request }: Route.LoaderArgs) {
87
// const user = await getUser(request);
@@ -71,78 +70,74 @@ export default function DashboardPage({
7170

7271
if (!data) {
7372
return (
74-
<DiscordLayout>
75-
<div className="h-full px-6 py-8">
76-
<div className="flex min-h-full justify-center">
77-
<RangeForm values={{ start, end }} />
78-
</div>
79-
<div></div>
73+
<div className="h-full px-6 py-8">
74+
<div className="flex justify-center">
75+
<RangeForm values={{ start, end }} />
8076
</div>
81-
</DiscordLayout>
77+
<div></div>
78+
</div>
8279
);
8380
}
8481

8582
return (
86-
<DiscordLayout>
87-
<div className="h-full px-6 py-8">
88-
<div className="flex min-h-full justify-center">
89-
<RangeForm values={{ start, end }} />
90-
</div>
91-
<div>
92-
<textarea
93-
defaultValue={`Author ID,Percent Zero Days,Word Count,Message Count,Channel Count,Category Count,Reaction Count,Word Score,Message Score,Channel Score,Consistency Score
83+
<div className="px-6 py-8">
84+
<div className="flex justify-center">
85+
<RangeForm values={{ start, end }} />
86+
</div>
87+
<div>
88+
<textarea
89+
defaultValue={`Author ID,Percent Zero Days,Word Count,Message Count,Channel Count,Category Count,Reaction Count,Word Score,Message Score,Channel Score,Consistency Score
9490
${data
9591
.map(
9692
(d) =>
9793
`${d.data.member.author_id},${d.metadata.percentZeroDays},${d.data.member.total_word_count},${d.data.member.message_count},${d.data.member.channel_count},${d.data.member.category_count},${d.data.member.total_reaction_count},${d.score.wordScore},${d.score.messageScore},${d.score.channelScore},${d.score.consistencyScore}`,
9894
)
9995
.join("\n")}`}
100-
></textarea>
101-
<table className="mt-24">
102-
<thead>
103-
<tr>
104-
<DataHeading>Author ID</DataHeading>
105-
<DataHeading>Percent Zero Days</DataHeading>
106-
<DataHeading>Word Count</DataHeading>
107-
<DataHeading>Message Count</DataHeading>
108-
<DataHeading>Channel Count</DataHeading>
109-
<DataHeading>Category Count</DataHeading>
110-
<DataHeading>Reaction Count</DataHeading>
111-
<DataHeading>Word Score</DataHeading>
112-
<DataHeading>Message Score</DataHeading>
113-
<DataHeading>Channel Score</DataHeading>
114-
<DataHeading>Consistency Score</DataHeading>
96+
></textarea>
97+
<table className="mt-24">
98+
<thead>
99+
<tr>
100+
<DataHeading>Author ID</DataHeading>
101+
<DataHeading>Percent Zero Days</DataHeading>
102+
<DataHeading>Word Count</DataHeading>
103+
<DataHeading>Message Count</DataHeading>
104+
<DataHeading>Channel Count</DataHeading>
105+
<DataHeading>Category Count</DataHeading>
106+
<DataHeading>Reaction Count</DataHeading>
107+
<DataHeading>Word Score</DataHeading>
108+
<DataHeading>Message Score</DataHeading>
109+
<DataHeading>Channel Score</DataHeading>
110+
<DataHeading>Consistency Score</DataHeading>
111+
</tr>
112+
</thead>
113+
<tbody>
114+
{data.map((d) => (
115+
<tr key={d.data.member.author_id}>
116+
<td>
117+
<Link
118+
to={{
119+
pathname: d.data.member.author_id,
120+
search: `?start=${start}&end=${end}`,
121+
}}
122+
>
123+
{d.data.member.username || d.data.member.author_id}
124+
</Link>
125+
</td>
126+
<td>{percent(d.metadata.percentZeroDays)}</td>
127+
<td>{d.data.member.total_word_count}</td>
128+
<td>{d.data.member.message_count}</td>
129+
<td>{d.data.member.channel_count}</td>
130+
<td>{d.data.member.category_count}</td>
131+
<td>{d.data.member.total_reaction_count}</td>
132+
<td>{d.score.wordScore}</td>
133+
<td>{d.score.messageScore}</td>
134+
<td>{d.score.channelScore}</td>
135+
<td>{d.score.consistencyScore}</td>
115136
</tr>
116-
</thead>
117-
<tbody>
118-
{data.map((d) => (
119-
<tr key={d.data.member.author_id}>
120-
<td>
121-
<Link
122-
to={{
123-
pathname: d.data.member.author_id,
124-
search: `?start=${start}&end=${end}`,
125-
}}
126-
>
127-
{d.data.member.username || d.data.member.author_id}
128-
</Link>
129-
</td>
130-
<td>{percent(d.metadata.percentZeroDays)}</td>
131-
<td>{d.data.member.total_word_count}</td>
132-
<td>{d.data.member.message_count}</td>
133-
<td>{d.data.member.channel_count}</td>
134-
<td>{d.data.member.category_count}</td>
135-
<td>{d.data.member.total_reaction_count}</td>
136-
<td>{d.score.wordScore}</td>
137-
<td>{d.score.messageScore}</td>
138-
<td>{d.score.channelScore}</td>
139-
<td>{d.score.consistencyScore}</td>
140-
</tr>
141-
))}
142-
</tbody>
143-
</table>
144-
</div>
137+
))}
138+
</tbody>
139+
</table>
145140
</div>
146-
</DiscordLayout>
141+
</div>
147142
);
148143
}

0 commit comments

Comments
 (0)