Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions app/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ export default function ForgotPasswordPage() {
return () => clearInterval(timer);
}, [rateLimitSeconds]);

// Redirect if already logged in
// Redirect if already logged in. Runs in an effect (not during render) so the
// navigation side effect stays out of the render phase.
useEffect(() => {
if (user) {
window.location.href = ROUTES.SETTINGS;
}
}, [user]);

if (user) {
window.location.href = ROUTES.SETTINGS;
return null;
}

Expand Down
1 change: 1 addition & 0 deletions app/auth/reset-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function ResetPasswordPage() {
// User should have a session from the email link
// Supabase handles the token exchange automatically
if (session) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncs a local validity flag to the external Supabase auth session
setIsValidSession(true);
} else {
// Give it a moment for the session to be established
Expand Down
10 changes: 8 additions & 2 deletions app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ export default function SignInPage() {
return () => clearInterval(timer);
}, [rateLimitSeconds]);

// Redirect if already logged in
// Redirect if already logged in. Runs in an effect (not during render) so the
// navigation side effect stays out of the render phase.
useEffect(() => {
if (user) {
window.location.href = ROUTES.SETTINGS;
}
}, [user]);

if (user) {
window.location.href = ROUTES.SETTINGS;
return null;
}

Expand Down
10 changes: 8 additions & 2 deletions app/auth/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ export default function SignUpPage() {
return () => clearInterval(timer);
}, [rateLimitSeconds]);

// Redirect if already logged in
// Redirect if already logged in. Runs in an effect (not during render) so the
// navigation side effect stays out of the render phase.
useEffect(() => {
if (user) {
window.location.href = ROUTES.SETTINGS;
}
}, [user]);

if (user) {
window.location.href = ROUTES.SETTINGS;
return null;
}

Expand Down
20 changes: 16 additions & 4 deletions app/auth/verify-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ export default function VerifyEmailPage() {
return () => clearInterval(timer);
}, [rateLimitSeconds]);

// Redirects run in effects (not during render) so the navigation side effects
// stay out of the render phase.
useEffect(() => {
if (!loading && !user) {
window.location.href = ROUTES.AUTH.SIGNIN;
}
}, [loading, user]);

useEffect(() => {
if (!loading && user && isEmailVerified) {
window.location.href = ROUTES.SETTINGS;
}
}, [loading, user, isEmailVerified]);

const isRateLimited = rateLimitSeconds > 0;

const handleResend = async () => {
Expand Down Expand Up @@ -72,15 +86,13 @@ export default function VerifyEmailPage() {
return <PageLoading />;
}

// Not logged in - redirect to signin
// Not logged in - the effect above redirects to signin.
if (!user) {
window.location.href = ROUTES.AUTH.SIGNIN;
return null;
}

// Already verified - redirect to settings
// Already verified - the effect above redirects to settings.
if (isEmailVerified) {
window.location.href = ROUTES.SETTINGS;
return null;
}

Expand Down
102 changes: 50 additions & 52 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,64 +74,62 @@ export default async function BlogPost({ params }: { params: Promise<{ slug: str
const { slug } = await params;
console.info('Rendering blog post for slug:', slug);

try {
if (!slug) {
console.info('Missing slug parameter');
notFound();
}
// No try/catch: notFound() and render errors must propagate to Next's
// not-found.tsx / error.tsx boundaries. A catch that only re-throws would also
// spuriously log notFound() as an error.
if (!slug) {
console.info('Missing slug parameter');
notFound();
}

const post = await fetchBlogPostBySlug(slug);
const post = await fetchBlogPostBySlug(slug);

if (!post) {
console.info('Blog post not found for slug:', slug);
notFound();
}
if (!post) {
console.info('Blog post not found for slug:', slug);
notFound();
}

return (
<article className="mx-auto max-w-3xl px-6 py-16">
<header className="mb-12">
<div className="flex items-center gap-4 text-sm text-gray-500">
<time dateTime={post.date}>{format(new Date(post.date), 'MMMM d, yyyy')}</time>
<span>•</span>
<span>{post.author}</span>
</div>
return (
<article className="mx-auto max-w-3xl px-6 py-16">
<header className="mb-12">
<div className="flex items-center gap-4 text-sm text-gray-500">
<time dateTime={post.date}>{format(new Date(post.date), 'MMMM d, yyyy')}</time>
<span>•</span>
<span>{post.author}</span>
</div>

<h1 className="mt-4 text-4xl font-semibold tracking-tight text-gray-900">{post.title}</h1>
<h1 className="mt-4 text-4xl font-semibold tracking-tight text-gray-900">{post.title}</h1>

{post.tags && post.tags.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="inline-block rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-700"
>
{tag}
</span>
))}
</div>
)}
{post.tags && post.tags.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="inline-block rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-700"
>
{tag}
</span>
))}
</div>
)}

{post.featuredImage && (
<div className="mt-8 relative h-96 w-full overflow-hidden rounded-lg">
<Image
src={post.featuredImage}
alt={post.title}
fill
sizes="(max-width: 1024px) 100vw, 800px"
className="object-cover"
priority
/>
</div>
)}
</header>
{post.featuredImage && (
<div className="mt-8 relative h-96 w-full overflow-hidden rounded-lg">
<Image
src={post.featuredImage}
alt={post.title}
fill
sizes="(max-width: 1024px) 100vw, 800px"
className="object-cover"
priority
/>
</div>
)}
</header>

<ServerMDXContent content={post.content} slug={post.slug} />
<ServerMDXContent content={post.content} slug={post.slug} />

<Comments slug={post.slug} />
</article>
);
} catch (error) {
console.info('Error rendering blog post:', error);
throw error;
}
<Comments slug={post.slug} />
</article>
);
}
1 change: 1 addition & 0 deletions app/projects/governance/components/CitizenProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const CitizenProfile: React.FC<CitizenProfileProps> = ({ citizen, agencies: _age
advisoryPercentage: contribution.percentage,
difference: 0,
}));
// eslint-disable-next-line react-hooks/set-state-in-effect -- seeds editable advisory distribution from the citizen prop
setAdvisoryDistribution(initialDistribution);
}
}, [citizen.contributions]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const CitizenProfile: React.FC<CitizenProfileProps> = ({ citizen, agencies: _age
advisoryPercentage: contribution.percentage,
difference: 0,
}));
// eslint-disable-next-line react-hooks/set-state-in-effect -- seeds editable advisory distribution from the citizen prop
setAdvisoryDistribution(initialDistribution);
}
}, [citizen.contributions]);
Expand Down
1 change: 1 addition & 0 deletions app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default function SettingsPage() {

// Initialize profile form when user data loads
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- seeds editable form fields from async-loaded user metadata
if (displayName) setProfileDisplayName(displayName);
if (avatarUrl) setProfileAvatarUrl(avatarUrl);
}, [displayName, avatarUrl]);
Expand Down
1 change: 1 addition & 0 deletions components/blog/Comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default function Comments({ slug }: { slug: string }) {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- client-mount detection for SSR-safe giscus embed
setIsClient(true);
}, []);

Expand Down
1 change: 1 addition & 0 deletions components/blog/MDXComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const Img = (
// passes string paths, so narrow before the string operations below.
if (!src || typeof src !== 'string') {
console.info('Image source missing');
// eslint-disable-next-line react-hooks/set-state-in-effect -- client-only image URL resolution (reads window.location as a slug fallback)
setIsError(true);
return;
}
Expand Down
1 change: 1 addition & 0 deletions components/conversations/ConversationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const ConversationList = ({
}, [botType, botId, documentId]);

useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loads conversations from the API on mount
loadConversations();
}, [loadConversations, refreshTrigger]);

Expand Down
1 change: 1 addition & 0 deletions components/shared/ProfessionalDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const ProfessionalDemo: FC<ProfessionalDemoProps> = ({ professional }) =>
role: 'assistant',
content: getWelcomeMessage(),
};
// eslint-disable-next-line react-hooks/set-state-in-effect -- seeds the on-mount welcome message
setMessages([welcomeMessage]);
inputRef.current?.focus({ preventScroll: true });
}, [getWelcomeMessage]);
Expand Down
1 change: 1 addition & 0 deletions components/shared/quick-create/QuickChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const QuickChat: FC<QuickChatProps> = ({ bot, onReset }) => {
role: 'assistant',
content: getWelcomeMessage(),
};
// eslint-disable-next-line react-hooks/set-state-in-effect -- seeds the on-mount welcome message
setMessages([welcomeMessage]);
inputRef.current?.focus({ preventScroll: true });
}, [getWelcomeMessage]);
Expand Down
9 changes: 0 additions & 9 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ const eslintConfig = [
'react/no-unescaped-entities': 'off',
'react/display-name': 'off',
'@next/next/no-img-element': 'warn',
// eslint-plugin-react-hooks v6 (bundled with Next 16's eslint-config-next)
// adds React-Compiler-era rules that did not exist in the Next 15 config and
// flag pre-existing intentional patterns (localStorage hydration, load-on-mount
// effects). Adopting them is a dedicated refactor, out of scope for this
// framework upgrade — deferred, tracked as follow-up. rules-of-hooks and
// exhaustive-deps stay enabled.
'react-hooks/set-state-in-effect': 'off',
'react-hooks/error-boundaries': 'off',
'react-hooks/immutability': 'off',
},
},
];
Expand Down
1 change: 1 addition & 0 deletions hooks/useDocumentChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const useDocumentChat = ({ selectedDocId }: UseDocumentChatOptions) => {
// Load conversation messages when active conversation changes
useEffect(() => {
if (!activeConversationId) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- resets messages when the active conversation clears
setChatMessages([]);
return;
}
Expand Down
1 change: 1 addition & 0 deletions lib/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {

// If Supabase is not configured, just set loading to false
if (!client) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- initializes auth state from the external Supabase client
setLoading(false);
return;
}
Expand Down
1 change: 1 addition & 0 deletions lib/hooks/useDashboardStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export function useDashboardStats(userId: string | undefined): UseDashboardStats
}, [userId]);

useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loads dashboard stats from the API on mount
loadData();
}, [loadData]);

Expand Down
1 change: 1 addition & 0 deletions lib/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function useLocalStorageFlag(
useEffect(() => {
const stored = localStorage.getItem(key);
if (stored !== null) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydrates from localStorage after mount (SSR-safe by design)
setValue(stored === 'true');
}
}, [key]);
Expand Down