From 4ebfc4c25412854027a6838ee898bb2d89db7297 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 5 Mar 2026 16:27:58 +0900 Subject: [PATCH] docs: add invoice optimization design plan Remove N+1 subscription invoice scanning, clean up billing service layer, and simplify InvoicesList component to single-purpose. --- apps/portal/next.config.mjs | 22 +- .../components/ServicesCarousel.tsx | 231 ++++++++---------- .../src/features/landing-page/hooks/index.ts | 2 +- .../landing-page/hooks/useInfiniteCarousel.ts | 164 +++---------- .../2026-03-05-invoice-optimization-design.md | 86 +++++++ 5 files changed, 238 insertions(+), 267 deletions(-) create mode 100644 docs/plans/2026-03-05-invoice-optimization-design.md diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs index 54e7685d..ed6db0e7 100644 --- a/apps/portal/next.config.mjs +++ b/apps/portal/next.config.mjs @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const workspaceRoot = path.resolve(__dirname, "..", ".."); +const domainDist = path.join(workspaceRoot, "packages/domain/dist"); // BFF URL for development API proxying const BFF_URL = process.env.BFF_URL || "http://localhost:4000"; @@ -19,32 +20,29 @@ const nextConfig = { turbopack: { resolveAlias: { - "@customer-portal/domain": path.join(workspaceRoot, "packages/domain/dist"), + "@customer-portal/domain": domainDist, }, }, webpack(config, { dev }) { config.resolve.alias = { ...config.resolve.alias, - "@customer-portal/domain": path.join(workspaceRoot, "packages/domain/dist"), + "@customer-portal/domain": domainDist, }; - // Watch domain package dist for changes in development if (dev) { config.watchOptions = { ...config.watchOptions, - ignored: config.watchOptions?.ignored - ? [ - ...(Array.isArray(config.watchOptions.ignored) - ? config.watchOptions.ignored - : [config.watchOptions.ignored]), - ].filter(p => !String(p).includes("packages/domain")) - : ["**/node_modules/**"], + // Poll-based watching is more reliable on WSL2 where inotify can miss events + poll: 1000, + aggregateTimeout: 300, + ignored: ["**/node_modules/!(@@customer-portal)/**"], }; - // Add domain dist to snapshot managed paths for better change detection + // Only exclude the domain dist from managed paths so webpack detects domain rebuilds + // without nuking the entire resolution cache (which causes crash loops) config.snapshot = { ...config.snapshot, - managedPaths: [], + managedPaths: [/[\\/]node_modules[\\/](?!@customer-portal[\\/])/], }; } diff --git a/apps/portal/src/features/landing-page/components/ServicesCarousel.tsx b/apps/portal/src/features/landing-page/components/ServicesCarousel.tsx index d621b2d2..433bc008 100644 --- a/apps/portal/src/features/landing-page/components/ServicesCarousel.tsx +++ b/apps/portal/src/features/landing-page/components/ServicesCarousel.tsx @@ -4,7 +4,7 @@ import { memo, useEffect, useState } from "react"; import Link from "next/link"; import { ArrowRight, ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "@/shared/utils"; -import { useInfiniteCarousel, useInView } from "@/features/landing-page/hooks"; +import { useCarousel, useInView } from "@/features/landing-page/hooks"; import { personalConversionCards, businessConversionCards, @@ -21,8 +21,8 @@ interface AccentStyles { iconText: string; ctaBg: string; dotBg: string; - borderInactive: string; - borderActive: string; + border: string; + glowFrom: string; cssVar: string; } @@ -30,153 +30,155 @@ const ACCENTS: Record = { blue: { iconBg: "bg-blue-500/12", iconText: "text-blue-600", - ctaBg: "bg-blue-600", + ctaBg: "bg-blue-600 hover:bg-blue-700", dotBg: "bg-blue-600", - borderInactive: "border-blue-500/10", - borderActive: "border-blue-500/25", + border: "border-blue-500/20", + glowFrom: "from-blue-500/5", cssVar: "var(--color-blue-500)", }, emerald: { iconBg: "bg-emerald-500/12", iconText: "text-emerald-600", - ctaBg: "bg-emerald-600", + ctaBg: "bg-emerald-600 hover:bg-emerald-700", dotBg: "bg-emerald-600", - borderInactive: "border-emerald-500/10", - borderActive: "border-emerald-500/25", + border: "border-emerald-500/20", + glowFrom: "from-emerald-500/5", cssVar: "var(--color-emerald-500)", }, violet: { iconBg: "bg-violet-500/12", iconText: "text-violet-600", - ctaBg: "bg-violet-600", + ctaBg: "bg-violet-600 hover:bg-violet-700", dotBg: "bg-violet-600", - borderInactive: "border-violet-500/10", - borderActive: "border-violet-500/25", + border: "border-violet-500/20", + glowFrom: "from-violet-500/5", cssVar: "var(--color-violet-500)", }, amber: { iconBg: "bg-amber-500/12", iconText: "text-amber-600", - ctaBg: "bg-amber-600", + ctaBg: "bg-amber-600 hover:bg-amber-700", dotBg: "bg-amber-600", - borderInactive: "border-amber-500/10", - borderActive: "border-amber-500/25", + border: "border-amber-500/20", + glowFrom: "from-amber-500/5", cssVar: "var(--color-amber-500)", }, indigo: { iconBg: "bg-indigo-500/12", iconText: "text-indigo-600", - ctaBg: "bg-indigo-600", + ctaBg: "bg-indigo-600 hover:bg-indigo-700", dotBg: "bg-indigo-600", - borderInactive: "border-indigo-500/10", - borderActive: "border-indigo-500/25", + border: "border-indigo-500/20", + glowFrom: "from-indigo-500/5", cssVar: "var(--color-indigo-500)", }, cyan: { iconBg: "bg-cyan-500/12", iconText: "text-cyan-600", - ctaBg: "bg-cyan-600", + ctaBg: "bg-cyan-600 hover:bg-cyan-700", dotBg: "bg-cyan-600", - borderInactive: "border-cyan-500/10", - borderActive: "border-cyan-500/25", + border: "border-cyan-500/20", + glowFrom: "from-cyan-500/5", cssVar: "var(--color-cyan-500)", }, rose: { iconBg: "bg-rose-500/12", iconText: "text-rose-600", - ctaBg: "bg-rose-600", + ctaBg: "bg-rose-600 hover:bg-rose-700", dotBg: "bg-rose-600", - borderInactive: "border-rose-500/10", - borderActive: "border-rose-500/25", + border: "border-rose-500/20", + glowFrom: "from-rose-500/5", cssVar: "var(--color-rose-500)", }, slate: { iconBg: "bg-slate-500/12", iconText: "text-slate-600", - ctaBg: "bg-slate-600", + ctaBg: "bg-slate-600 hover:bg-slate-700", dotBg: "bg-slate-600", - borderInactive: "border-slate-500/10", - borderActive: "border-slate-500/25", + border: "border-slate-500/20", + glowFrom: "from-slate-500/5", cssVar: "var(--color-slate-500)", }, }; -/* ─── Slide visual styles by distance from center ─── */ +/* ─── Crossfade Card ─── */ -const SLIDE_STYLES = [ - { scale: 1, opacity: 1, filter: "none" }, - { scale: 0.88, opacity: 0.5, filter: "blur(2px)" }, - { scale: 0.78, opacity: 0.25, filter: "blur(4px)" }, -] as const; - -function slideStyle(offset: number) { - return SLIDE_STYLES[Math.min(offset, 2)] ?? SLIDE_STYLES[2]!; -} - -const GAP = 24; - -/* ─── Spotlight Card ─── */ - -const SpotlightCard = memo(function SpotlightCard({ - card, - isActive, -}: { - card: ConversionServiceCard; - isActive: boolean; -}) { +const CrossfadeCard = memo(function CrossfadeCard({ card }: { card: ConversionServiceCard }) { const a = ACCENTS[card.accent]; return ( { - if (!isActive) e.preventDefault(); - }} className={cn( - "block h-full rounded-3xl border overflow-hidden transition-shadow duration-500", - isActive - ? cn("shadow-xl hover:shadow-2xl", a.borderActive) - : cn("shadow-sm", a.borderInactive) + "block rounded-3xl border overflow-hidden", + "shadow-lg hover:shadow-xl transition-shadow duration-300", + a.border )} style={{ - background: `linear-gradient(145deg, color-mix(in oklch, ${a.cssVar} 8%, white), white)`, + background: `linear-gradient(145deg, color-mix(in oklch, ${a.cssVar} 6%, white), white)`, }} > -
-
-
+ {/* Left: Content */} +
+
+
+
{card.icon}
+
+ {card.badge && ( + + {card.badge} + + )} +
+ +

{card.problemHook}

+

+ {card.title} +

+

+ {card.description} +

+ + -
{card.icon}
-
- {card.badge && ( - - {card.badge} - - )} + {card.ctaLabel} + +
-

{card.problemHook}

-

- {card.title} -

-

- {card.description} -

- - {card.ctaLabel} - - +
+
+
{card.icon}
+
+

{card.keyBenefit}

+
+
); @@ -192,7 +194,7 @@ function CarouselHeader({ onTabChange: (tab: Tab) => void; }) { return ( -
+

Our Services

@@ -238,7 +240,7 @@ function CarouselNav({ goNext: () => void; }) { return ( -
+