perf: optimize DB queries, SSE polling, and client rendering
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m45s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m45s
- Fix resolveCollaborator N+1: replace full User table scan with findFirst - Fix getAllUsersWithStats N+1: use groupBy instead of per-user count queries - Cache getTeamMemberIdsForAdminTeams and isAdminOfUser with React.cache - Increase SSE poll interval from 1s to 2s across all 5 subscribe routes - Add cleanupOldEvents method to session-share-events for event table TTL - Add React.memo to all card components (Swot, Motivator, Weather, WeeklyCheckIn, YearReview) - Fix WeatherCard useEffect+setState lint error with idiomatic prop sync pattern - Add optimizePackageImports for DnD libs and poweredByHeader:false in next.config - Add inline theme script in layout.tsx to prevent dark mode FOUC - Remove unused Next.js template SVGs from public/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type { MotivatorCard as MotivatorCardType } from '@/lib/types';
|
||||
@@ -12,7 +13,7 @@ interface MotivatorCardProps {
|
||||
showInfluence?: boolean;
|
||||
}
|
||||
|
||||
export function MotivatorCard({
|
||||
export const MotivatorCard = memo(function MotivatorCard({
|
||||
card,
|
||||
disabled = false,
|
||||
showInfluence = false,
|
||||
@@ -87,10 +88,10 @@ export function MotivatorCard({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Non-draggable version for summary
|
||||
export function MotivatorCardStatic({
|
||||
export const MotivatorCardStatic = memo(function MotivatorCardStatic({
|
||||
card,
|
||||
size = 'normal',
|
||||
}: {
|
||||
@@ -156,4 +157,4 @@ export function MotivatorCardStatic({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef, useState, useTransition } from 'react';
|
||||
import { forwardRef, memo, useState, useTransition } from 'react';
|
||||
import type { SwotItem, SwotCategory } from '@prisma/client';
|
||||
import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot';
|
||||
|
||||
@@ -21,7 +21,7 @@ const categoryStyles: Record<SwotCategory, { ring: string; text: string }> = {
|
||||
THREAT: { ring: 'ring-threat', text: 'text-threat' },
|
||||
};
|
||||
|
||||
export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
|
||||
export const SwotCard = memo(forwardRef<HTMLDivElement, SwotCardProps>(
|
||||
(
|
||||
{ item, sessionId, isSelected, isHighlighted, isDragging, linkMode, onSelect, ...props },
|
||||
ref
|
||||
@@ -196,6 +196,5 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
));
|
||||
SwotCard.displayName = 'SwotCard';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition, useEffect } from 'react';
|
||||
import { memo, useState, useTransition } from 'react';
|
||||
import { createOrUpdateWeatherEntry } from '@/actions/weather';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
@@ -89,25 +89,28 @@ function EvolutionIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
export function WeatherCard({ sessionId, currentUserId, entry, canEdit, previousEntry }: WeatherCardProps) {
|
||||
export const WeatherCard = memo(function WeatherCard({ sessionId, currentUserId, entry, canEdit, previousEntry }: WeatherCardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
// Track entry version to reset local state when props change (SSE refresh)
|
||||
const [entryVersion, setEntryVersion] = useState(entry);
|
||||
const [notes, setNotes] = useState(entry.notes || '');
|
||||
const [performanceEmoji, setPerformanceEmoji] = useState(entry.performanceEmoji || null);
|
||||
const [moralEmoji, setMoralEmoji] = useState(entry.moralEmoji || null);
|
||||
const [fluxEmoji, setFluxEmoji] = useState(entry.fluxEmoji || null);
|
||||
const [valueCreationEmoji, setValueCreationEmoji] = useState(entry.valueCreationEmoji || null);
|
||||
|
||||
const isCurrentUser = entry.userId === currentUserId;
|
||||
const canEditThis = canEdit && isCurrentUser;
|
||||
|
||||
// Sync local state with props when they change (e.g., from SSE refresh)
|
||||
useEffect(() => {
|
||||
// Reset local state when entry props change (React-idiomatic pattern)
|
||||
if (entryVersion !== entry) {
|
||||
setEntryVersion(entry);
|
||||
setNotes(entry.notes || '');
|
||||
setPerformanceEmoji(entry.performanceEmoji || null);
|
||||
setMoralEmoji(entry.moralEmoji || null);
|
||||
setFluxEmoji(entry.fluxEmoji || null);
|
||||
setValueCreationEmoji(entry.valueCreationEmoji || null);
|
||||
}, [entry.notes, entry.performanceEmoji, entry.moralEmoji, entry.fluxEmoji, entry.valueCreationEmoji]);
|
||||
}
|
||||
|
||||
const isCurrentUser = entry.userId === currentUserId;
|
||||
const canEditThis = canEdit && isCurrentUser;
|
||||
|
||||
function handleEmojiChange(axis: 'performance' | 'moral' | 'flux' | 'valueCreation', emoji: string | null) {
|
||||
if (!canEditThis) return;
|
||||
@@ -282,4 +285,4 @@ export function WeatherCard({ sessionId, currentUserId, entry, canEdit, previous
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef, useState, useTransition } from 'react';
|
||||
import { forwardRef, memo, useState, useTransition } from 'react';
|
||||
import type { WeeklyCheckInItem } from '@prisma/client';
|
||||
import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin';
|
||||
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
|
||||
@@ -12,7 +12,7 @@ interface WeeklyCheckInCardProps {
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
export const WeeklyCheckInCard = forwardRef<HTMLDivElement, WeeklyCheckInCardProps>(
|
||||
export const WeeklyCheckInCard = memo(forwardRef<HTMLDivElement, WeeklyCheckInCardProps>(
|
||||
({ item, sessionId, isDragging, ...props }, ref) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [content, setContent] = useState(item.content);
|
||||
@@ -195,6 +195,5 @@ export const WeeklyCheckInCard = forwardRef<HTMLDivElement, WeeklyCheckInCardPro
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
));
|
||||
WeeklyCheckInCard.displayName = 'WeeklyCheckInCard';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef, useState, useTransition } from 'react';
|
||||
import { forwardRef, memo, useState, useTransition } from 'react';
|
||||
import type { YearReviewItem } from '@prisma/client';
|
||||
import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review';
|
||||
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
|
||||
@@ -11,7 +11,7 @@ interface YearReviewCardProps {
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
export const YearReviewCard = forwardRef<HTMLDivElement, YearReviewCardProps>(
|
||||
export const YearReviewCard = memo(forwardRef<HTMLDivElement, YearReviewCardProps>(
|
||||
({ item, sessionId, isDragging, ...props }, ref) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [content, setContent] = useState(item.content);
|
||||
@@ -126,6 +126,5 @@ export const YearReviewCard = forwardRef<HTMLDivElement, YearReviewCardProps>(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
));
|
||||
YearReviewCard.displayName = 'YearReviewCard';
|
||||
|
||||
Reference in New Issue
Block a user