chore: add pre-commit hooks, tighten ESLint, activate Sentry DSN, publish CI coverage (Phase 1)
- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files - Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error - Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin - Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments - Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example - Add coverage artifact upload step to CI test job - Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
type DashboardLayoutConfig,
|
||||
type DashboardWidgetType,
|
||||
} from "@capakraken/shared/types";
|
||||
import { type DashboardLayoutConfig, type DashboardWidgetType } from "@capakraken/shared/types";
|
||||
import {
|
||||
createDashboardWidget,
|
||||
createDefaultDashboardLayout,
|
||||
@@ -46,14 +43,15 @@ export function shouldHydrateDashboardFromDb(params: {
|
||||
hasLocalChangesBeforeHydration: boolean;
|
||||
}): boolean {
|
||||
const { remoteLayout, hasHydratedFromDb, hasLocalChangesBeforeHydration } = params;
|
||||
return remoteLayout !== null
|
||||
&& remoteLayout !== undefined
|
||||
&& !hasHydratedFromDb
|
||||
&& !hasLocalChangesBeforeHydration;
|
||||
return (
|
||||
remoteLayout !== null &&
|
||||
remoteLayout !== undefined &&
|
||||
!hasHydratedFromDb &&
|
||||
!hasLocalChangesBeforeHydration
|
||||
);
|
||||
}
|
||||
|
||||
export function useDashboardLayout() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { data: meData } = trpc.user.me.useQuery() as { data: { id?: string } | null | undefined };
|
||||
const userId = meData?.id ?? null;
|
||||
|
||||
@@ -74,7 +72,12 @@ export function useDashboardLayout() {
|
||||
|
||||
// Once userId is known, hydrate from user-scoped localStorage (if no DB data yet).
|
||||
useEffect(() => {
|
||||
if (!userId || hasHydratedFromStorageRef.current || hasHydratedFromDbRef.current || hasLocalChangesBeforeHydrationRef.current) {
|
||||
if (
|
||||
!userId ||
|
||||
hasHydratedFromStorageRef.current ||
|
||||
hasHydratedFromDbRef.current ||
|
||||
hasLocalChangesBeforeHydrationRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stored = loadFromStorage(userId);
|
||||
@@ -90,7 +93,6 @@ export function useDashboardLayout() {
|
||||
setIsHydrated(true);
|
||||
}, [userId]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { data: dbData } = trpc.user.getDashboardLayout.useQuery(undefined, {
|
||||
staleTime: 30_000,
|
||||
}) as { data: { layout: DashboardLayoutConfig | null; updatedAt: unknown } | null | undefined };
|
||||
@@ -122,11 +124,13 @@ export function useDashboardLayout() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldHydrateDashboardFromDb({
|
||||
remoteLayout,
|
||||
hasHydratedFromDb: hasHydratedFromDbRef.current,
|
||||
hasLocalChangesBeforeHydration: hasLocalChangesBeforeHydrationRef.current,
|
||||
})) {
|
||||
if (
|
||||
!shouldHydrateDashboardFromDb({
|
||||
remoteLayout,
|
||||
hasHydratedFromDb: hasHydratedFromDbRef.current,
|
||||
hasLocalChangesBeforeHydration: hasLocalChangesBeforeHydrationRef.current,
|
||||
})
|
||||
) {
|
||||
// DB data present but local changes already in-flight — keep local state, mark done.
|
||||
hasHydratedFromDbRef.current = true;
|
||||
setIsHydrated(true);
|
||||
@@ -159,69 +163,84 @@ export function useDashboardLayout() {
|
||||
|
||||
// Flush any pending debounced DB save when the component unmounts so that
|
||||
// navigating away within the 2-second window doesn't silently lose changes.
|
||||
useEffect(() => () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
saveTimeoutRef.current = null;
|
||||
if (pendingLayoutSaveRef.current) {
|
||||
saveMutationRef.current.mutate({ layout: pendingLayoutSaveRef.current });
|
||||
pendingLayoutSaveRef.current = null;
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
saveTimeoutRef.current = null;
|
||||
if (pendingLayoutSaveRef.current) {
|
||||
saveMutationRef.current.mutate({ layout: pendingLayoutSaveRef.current });
|
||||
pendingLayoutSaveRef.current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const persist = useCallback((nextConfig: DashboardLayoutConfig) => {
|
||||
if (!hasHydratedFromDbRef.current) {
|
||||
hasLocalChangesBeforeHydrationRef.current = true;
|
||||
}
|
||||
const newConfig = normalizeDashboardLayout(nextConfig);
|
||||
if (userId) saveToStorage(userId, newConfig);
|
||||
pendingLayoutSaveRef.current = newConfig;
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
pendingLayoutSaveRef.current = null;
|
||||
saveMutation.mutate({ layout: newConfig });
|
||||
}, 2000);
|
||||
}, [saveMutation, userId]);
|
||||
const persist = useCallback(
|
||||
(nextConfig: DashboardLayoutConfig) => {
|
||||
if (!hasHydratedFromDbRef.current) {
|
||||
hasLocalChangesBeforeHydrationRef.current = true;
|
||||
}
|
||||
const newConfig = normalizeDashboardLayout(nextConfig);
|
||||
if (userId) saveToStorage(userId, newConfig);
|
||||
pendingLayoutSaveRef.current = newConfig;
|
||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
pendingLayoutSaveRef.current = null;
|
||||
saveMutation.mutate({ layout: newConfig });
|
||||
}, 2000);
|
||||
},
|
||||
[saveMutation, userId],
|
||||
);
|
||||
|
||||
const addWidget = useCallback((type: DashboardWidgetType) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
widgets: [
|
||||
...prev.widgets,
|
||||
createDashboardWidget(type, {
|
||||
id: generateWidgetId(),
|
||||
x: 0,
|
||||
y: getNextDashboardWidgetY(prev.widgets),
|
||||
}),
|
||||
],
|
||||
};
|
||||
persist(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
}, [persist]);
|
||||
const addWidget = useCallback(
|
||||
(type: DashboardWidgetType) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
widgets: [
|
||||
...prev.widgets,
|
||||
createDashboardWidget(type, {
|
||||
id: generateWidgetId(),
|
||||
x: 0,
|
||||
y: getNextDashboardWidgetY(prev.widgets),
|
||||
}),
|
||||
],
|
||||
};
|
||||
persist(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
},
|
||||
[persist],
|
||||
);
|
||||
|
||||
const removeWidget = useCallback((id: string) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = { ...prev, widgets: prev.widgets.filter((w) => w.id !== id) };
|
||||
persist(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
}, [persist]);
|
||||
const removeWidget = useCallback(
|
||||
(id: string) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = { ...prev, widgets: prev.widgets.filter((w) => w.id !== id) };
|
||||
persist(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
},
|
||||
[persist],
|
||||
);
|
||||
|
||||
const updateWidgetConfig = useCallback((id: string, configUpdate: Record<string, unknown>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
widgets: prev.widgets.map((w) =>
|
||||
w.id === id ? { ...w, config: { ...w.config, ...configUpdate } } : w,
|
||||
),
|
||||
};
|
||||
persist(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
}, [persist]);
|
||||
const updateWidgetConfig = useCallback(
|
||||
(id: string, configUpdate: Record<string, unknown>) => {
|
||||
setConfig((prev) => {
|
||||
const newConfig = {
|
||||
...prev,
|
||||
widgets: prev.widgets.map((w) =>
|
||||
w.id === id ? { ...w, config: { ...w.config, ...configUpdate } } : w,
|
||||
),
|
||||
};
|
||||
persist(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
},
|
||||
[persist],
|
||||
);
|
||||
|
||||
const onLayoutChange = useCallback(
|
||||
(layout: { i: string; x: number; y: number; w: number; h: number }[]) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { trpc } from "~/lib/trpc/client.js";
|
||||
* Fetches full project context when a project is being dragged or the panel opens.
|
||||
* Returns the project's resources, their own allocations, and all cross-project allocations.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
type ProjectDragContextResult = {
|
||||
contextResourceIds: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -18,8 +18,10 @@ type ProjectDragContextResult = {
|
||||
project: any | null;
|
||||
};
|
||||
|
||||
export function useProjectDragContext(projectId: string | null, enabled = true): ProjectDragContextResult {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useProjectDragContext(
|
||||
projectId: string | null,
|
||||
enabled = true,
|
||||
): ProjectDragContextResult {
|
||||
const { data } = trpc.timeline.getProjectContext.useQuery(
|
||||
{ projectId: projectId! },
|
||||
{ enabled: enabled && !!projectId, staleTime: 10_000 },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
|
||||
interface UseTimelineKeyboardOptions {
|
||||
|
||||
@@ -35,23 +35,29 @@ export function useTimelineLayout(
|
||||
);
|
||||
|
||||
// Grid lines — memoized; identical for every row
|
||||
const gridLines = useMemo(() => dates.map((date, i) => {
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
const dow = date.getDay();
|
||||
const isWeekend = dow === 0 || dow === 6;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={clsx(
|
||||
"absolute top-0 bottom-0 border-r",
|
||||
isToday ? "border-brand-300 dark:border-brand-700 border-r-2" :
|
||||
isWeekend ? "border-brand-200 dark:border-brand-800 bg-brand-50/40 dark:bg-brand-950/20" :
|
||||
"border-gray-100 dark:border-gray-800",
|
||||
)}
|
||||
style={{ left: i * CELL_WIDTH, width: CELL_WIDTH }}
|
||||
/>
|
||||
);
|
||||
}), [dates, CELL_WIDTH, today]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const gridLines = useMemo(
|
||||
() =>
|
||||
dates.map((date, i) => {
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
const dow = date.getDay();
|
||||
const isWeekend = dow === 0 || dow === 6;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={clsx(
|
||||
"absolute top-0 bottom-0 border-r",
|
||||
isToday
|
||||
? "border-brand-300 dark:border-brand-700 border-r-2"
|
||||
: isWeekend
|
||||
? "border-brand-200 dark:border-brand-800 bg-brand-50/40 dark:bg-brand-950/20"
|
||||
: "border-gray-100 dark:border-gray-800",
|
||||
)}
|
||||
style={{ left: i * CELL_WIDTH, width: CELL_WIDTH }}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
[dates, CELL_WIDTH, today],
|
||||
);
|
||||
|
||||
// Month groups for the month header
|
||||
const monthGroups = useMemo(() => {
|
||||
@@ -72,5 +78,15 @@ export function useTimelineLayout(
|
||||
return dates[colIndex] ?? today;
|
||||
}
|
||||
|
||||
return { CELL_WIDTH, dates, visibleDays, totalCanvasWidth, toLeft, toWidth, gridLines, monthGroups, xToDate };
|
||||
return {
|
||||
CELL_WIDTH,
|
||||
dates,
|
||||
visibleDays,
|
||||
totalCanvasWidth,
|
||||
toLeft,
|
||||
toWidth,
|
||||
gridLines,
|
||||
monthGroups,
|
||||
xToDate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
|
||||
type PopoverAnchor =
|
||||
| { kind: "point"; x: number; y: number }
|
||||
@@ -113,8 +114,8 @@ export function useViewportPopover({
|
||||
return;
|
||||
}
|
||||
if (
|
||||
target instanceof Element
|
||||
&& ignoreSelectors.some((selector) => target.closest(selector) !== null)
|
||||
target instanceof Element &&
|
||||
ignoreSelectors.some((selector) => target.closest(selector) !== null)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -190,9 +191,7 @@ export function useViewportPopover({
|
||||
if (
|
||||
ignoreScrollContainers?.some(
|
||||
(r) =>
|
||||
r.current != null &&
|
||||
scrollTarget instanceof Node &&
|
||||
r.current.contains(scrollTarget),
|
||||
r.current != null && scrollTarget instanceof Node && r.current.contains(scrollTarget),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user