feat(web): add error boundaries, loading skeletons, render fixes and tree-shaking
- Add error.tsx to all 13 route groups: admin, allocations, analytics, dashboard, estimates, notifications, projects, reports, resources, roles, staffing, timeline, vacations - Add loading.tsx to 9 routes that were missing them: admin, analytics, dashboard, estimates, notifications, reports, roles, staffing, vacations - ResourceDetail: wrap vacationStart in useMemo to stabilize query key, remove dead windowEnd variable - node-renderer.ts: replace barrel import (import * as THREE) with named imports for tree-shaking - next.config.ts: add framer-motion and @capakraken/shared to optimizePackageImports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ const nextConfig: NextConfig = {
|
||||
outputFileTracingRoot: path.resolve(__dirname, "../.."),
|
||||
devIndicators: false,
|
||||
experimental: {
|
||||
optimizePackageImports: ["recharts", "date-fns"],
|
||||
optimizePackageImports: ["recharts", "date-fns", "framer-motion", "@capakraken/shared"],
|
||||
},
|
||||
transpilePackages: [
|
||||
"@capakraken/api",
|
||||
@@ -15,7 +15,6 @@ const nextConfig: NextConfig = {
|
||||
"@capakraken/engine",
|
||||
"@capakraken/shared",
|
||||
"@capakraken/staffing",
|
||||
"@capakraken/ui",
|
||||
],
|
||||
typedRoutes: true,
|
||||
async redirects() {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export default function RouteError({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">{error.message}</p>
|
||||
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4 animate-pulse">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as THREE from "three";
|
||||
import { CanvasTexture, Sprite, SpriteMaterial } from "three";
|
||||
import type { PositionedNode } from "./graph-data";
|
||||
|
||||
// ─── Canvas-based node sprites ──────────────────────────────────────────────
|
||||
|
||||
const spriteCache = new Map<string, THREE.Sprite>();
|
||||
const spriteCache = new Map<string, Sprite>();
|
||||
|
||||
/**
|
||||
* Creates a Three.js sprite for a graph node: colored circle with value label.
|
||||
*/
|
||||
export function createNodeSprite(node: PositionedNode): THREE.Sprite {
|
||||
export function createNodeSprite(node: PositionedNode): Sprite {
|
||||
const cacheKey = `${node.id}:${node.value}:${node.color}`;
|
||||
const cached = spriteCache.get(cacheKey);
|
||||
if (cached) return cached.clone();
|
||||
@@ -78,13 +78,13 @@ export function createNodeSprite(node: PositionedNode): THREE.Sprite {
|
||||
ctx.fillText(node.unit, cx, cy + 60);
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const material = new THREE.SpriteMaterial({
|
||||
const texture = new CanvasTexture(canvas);
|
||||
const material = new SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
const sprite = new THREE.Sprite(material);
|
||||
const sprite = new Sprite(material);
|
||||
sprite.scale.set(50, 50, 1);
|
||||
|
||||
spriteCache.set(cacheKey, sprite);
|
||||
@@ -94,7 +94,7 @@ export function createNodeSprite(node: PositionedNode): THREE.Sprite {
|
||||
/**
|
||||
* Creates a dimmed version of a node sprite (for non-highlighted nodes).
|
||||
*/
|
||||
export function createDimmedNodeSprite(node: PositionedNode): THREE.Sprite {
|
||||
export function createDimmedNodeSprite(node: PositionedNode): Sprite {
|
||||
const sprite = createNodeSprite({ ...node, color: "#4b5563" });
|
||||
sprite.material.opacity = 0.3;
|
||||
return sprite;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { AllocationLike, AllocationReadModel, AllocationWithDetails, Resource, SkillEntry } from "@capakraken/shared";
|
||||
@@ -96,8 +96,6 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
|
||||
// Fetch allocations for this resource (all non-cancelled)
|
||||
const now = new Date();
|
||||
const windowEnd = new Date(now);
|
||||
windowEnd.setDate(windowEnd.getDate() + 90);
|
||||
|
||||
const _allocQuery = trpc.allocation.listView.useQuery(
|
||||
{ resourceId },
|
||||
@@ -110,8 +108,12 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
const loadingAllocations = _allocQuery.isLoading;
|
||||
|
||||
// Fetch upcoming/recent vacations
|
||||
const vacationStart = new Date(now);
|
||||
vacationStart.setMonth(vacationStart.getMonth() - 1);
|
||||
const vacationStart = useMemo(() => {
|
||||
const d = new Date();
|
||||
d.setMonth(d.getMonth() - 1);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}, []);
|
||||
|
||||
const { data: vacations, isLoading: loadingVacations } = trpc.vacation.list.useQuery(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user