From 27dcc218ce4acdf567ec56986b2ffd119c0c26ea Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 27 May 2026 16:49:57 +0000 Subject: [PATCH] visual improvement on groups --- .../your-projects/live/AnalyticsView.tsx | 4 +- .../your-projects/live/GroupFilter.tsx | 159 +++++++++++++++--- .../your-projects/live/LiveTracker.tsx | Bin 20688 -> 22565 bytes 3 files changed, 134 insertions(+), 29 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx index f99dec8..6479501 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/AnalyticsView.tsx @@ -8,7 +8,7 @@ import SurveyedResultsPieChart from "./SurveyedResultsPieChart"; import DampMouldRiskPanel from "./DampMouldRiskPanel"; import CompletionTrendsChart from "./CompletionTrendsChart"; import SurveyIssuesPanel from "./SurveyIssuesPanel"; -import GroupFilter, { type GroupOption } from "./GroupFilter"; +import GroupFilter, { type GroupNode } from "./GroupFilter"; import { STAGE_COLORS, STAGE_ORDER } from "./types"; import type { ProjectData, @@ -313,7 +313,7 @@ interface AnalyticsViewProps { ) => void; majorConditionDeals: ClassifiedDeal[]; totalDeals: number; - availableGroups: GroupOption[]; + availableGroups: GroupNode[]; groupFilter: string[]; onGroupFilterChange: (next: string[]) => void; groupFilterActive: boolean; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/GroupFilter.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/GroupFilter.tsx index 3832c2c..d9714da 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/GroupFilter.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/GroupFilter.tsx @@ -1,6 +1,7 @@ "use client"; -import { ChevronDown, Layers } from "lucide-react"; +import { Fragment } from "react"; +import { ChevronDown, Layers, Minus } from "lucide-react"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -11,14 +12,28 @@ import { } from "@/app/shadcn_components/ui/dropdown-menu"; import { Card } from "@/app/shadcn_components/ui/card"; -export type GroupOption = { +export type GroupLeaf = { value: string; + // Full label, used in the trigger when this leaf is selected outside the + // context of a fully-checked parent. label: string; + // Short label rendered under a parent header (the parent already provides + // context). Falls back to `label`. + shortLabel?: string; muted?: boolean; }; +export type GroupNode = + | { kind: "leaf"; leaf: GroupLeaf } + | { + kind: "parent"; + label: string; + muted?: boolean; + children: GroupLeaf[]; + }; + interface GroupFilterProps { - options: GroupOption[]; + options: GroupNode[]; selected: string[]; onChange: (next: string[]) => void; variant?: "card" | "inline"; @@ -34,7 +49,9 @@ function GroupDropdown({ triggerClassName: string; align?: "start" | "end"; }) { - const toggle = (value: string, checked: boolean) => { + const selectedSet = new Set(selected); + + const toggleLeaf = (value: string, checked: boolean) => { if (checked) { onChange([...selected, value]); } else { @@ -42,16 +59,44 @@ function GroupDropdown({ } }; - const selectedLabels = selected - .map((v) => options.find((o) => o.value === v)?.label) - .filter((l): l is string => l !== undefined); + const toggleParent = (childValues: string[]) => { + const allChecked = childValues.every((v) => selectedSet.has(v)); + if (allChecked) { + const remove = new Set(childValues); + onChange(selected.filter((v) => !remove.has(v))); + } else { + const merged = new Set(selected); + for (const v of childValues) merged.add(v); + onChange(Array.from(merged)); + } + }; + + // Trigger chunks: fully-selected parents collapse to the parent's label; + // standalone leaves and partial-parent children contribute their own labels. + const triggerChunks: string[] = []; + for (const node of options) { + if (node.kind === "leaf") { + if (selectedSet.has(node.leaf.value)) triggerChunks.push(node.leaf.label); + } else { + const childValues = node.children.map((c) => c.value); + const allSelected = + childValues.length > 0 && childValues.every((v) => selectedSet.has(v)); + if (allSelected) { + triggerChunks.push(node.label); + } else { + for (const child of node.children) { + if (selectedSet.has(child.value)) triggerChunks.push(child.label); + } + } + } + } const triggerLabel = - selectedLabels.length === 0 + triggerChunks.length === 0 ? "All groups" - : selectedLabels.length === 1 - ? selectedLabels[0] - : `${selectedLabels[0]} +${selectedLabels.length - 1}`; + : triggerChunks.length === 1 + ? triggerChunks[0] + : `${triggerChunks[0]} +${triggerChunks.length - 1}`; return ( @@ -66,7 +111,7 @@ function GroupDropdown({ Groups @@ -82,21 +127,81 @@ function GroupDropdown({
- {options.map((opt) => ( - toggle(opt.value, !!val)} - onSelect={(e) => e.preventDefault()} - className="text-sm" - > - {opt.muted ? ( - {opt.label} - ) : ( - opt.label - )} - - ))} + {options.map((node, i) => { + const needsSeparator = i > 0; + if (node.kind === "leaf") { + return ( + + {needsSeparator && } + toggleLeaf(node.leaf.value, !!v)} + onSelect={(e) => e.preventDefault()} + className="text-sm" + > + {node.leaf.muted ? ( + + {node.leaf.label} + + ) : ( + node.leaf.label + )} + + + ); + } + + const childValues = node.children.map((c) => c.value); + const selectedCount = childValues.filter((v) => + selectedSet.has(v), + ).length; + const parentState: boolean | "indeterminate" = + selectedCount === 0 + ? false + : selectedCount === childValues.length + ? true + : "indeterminate"; + + return ( + + {needsSeparator && } + toggleParent(childValues)} + onSelect={(e) => e.preventDefault()} + className="text-sm font-semibold" + > + {parentState === "indeterminate" && ( + + + + )} + {node.muted ? ( + {node.label} + ) : ( + node.label + )} + + {node.children.map((child) => ( + toggleLeaf(child.value, !!v)} + onSelect={(e) => e.preventDefault()} + className="text-sm pl-12" + > + {child.muted ? ( + + {child.shortLabel ?? child.label} + + ) : ( + (child.shortLabel ?? child.label) + )} + + ))} + + ); + })}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index 225fee1f116afa88298e21b42c69d68eaf3be5af..dd8b3bf4e046d27e068de2fb21edd7c57d5d80d5 100644 GIT binary patch delta 3013 zcmb_e&2HRO6js$xq|gwMy6Ga)iG_-dCT_z5!Az%7lT;v-qDoys6e)M?GZ~ZXANgJ< z8bu?+D=_R43l{7uqdY5WQMu)mWlxISMKpJce8D7Fml#fk; zCw?$;Vr=6f=zhm16n}iOd}rfyO=U0n##W*}ge;DIeG!`t?Ujp4%={}|QjM;I2JKM~ zI|M%t4XJy3;CVqvv!s5|h3P4le{6Vl^K4-oEp(}53VwqT$Lvhb;4yp}K#8&{GrIC< zUB{8A_p9g}Nq2LDl$OzmnnH>!6X218I(>QIvx&egLYU~hIxrj<1Sj)-C0`5Tb5>Xo z2Mt^;np0AtZ;}x5X@gDx4Kn6CPAZ^kwr+Ta(no_#00hnPNo~jhP?4d=7F)=hG|{CE zbYKrF)5(sdXeD8-^dt&QJJYSH8zZk#qEBCUcs@%DIp1+Ngt7iKDTSo9Lp}q>iy51s zOd`g~VXn4aICbL7yi^eQ%xZ1`6Z9I6bF`Lf8b;R+c}~ONL7uAh=bMHN&K4aj620MKwE@J?Y+&_S zd+BLevUxZy^VDxZKF7K6gTOi@6aHB#VdOsnzQ>7B=DP z(XeH+DPot}MYysrYMVrA;zPJFH1EObM^_idCky!Nho3e1Zu1;@Jx~zS@hAA=KgV4Vqy6g50 zuK0HLs=?)Tu>D`C5&vRFiac7u-x5I#t2eN-41yj601IcS12~740=3xn+}fjN`PwZ- zvaI!PSGs)EI#eTy#$blX8&lg%N72?MB& delta 1233 zcma)6L2lDP6jfDG5ENLTqJji^L{()=sjIdtQrfC2RaXcQ5(^M&CeEaBJ04pzV_K7h za0hdNF4$La0gk{0*l+?C9Dwf`Cn>0lT8cFJ^Z$GAzyIwoXH!2;r_OFo&pb~}l*n(j z>G>=cinUDzYg)OqL414kp3l)Z}PSE|Ey8w2g<&Fllqrb`pxLO$nK7bA{SAVZBhv zArFnNRSFY@<1UwFZSnI)eMmjToXJQY2(GmsINn!@G7ZX6oEWn3hy>x+r_W=tok(d@ zKi|w!B1I0#Pbl2UgvggzOLJ))o?s-m1YjcVvKBW{NZlSs=2|x*XUNmy{_LB|>f&T} zXAkt4JmuJxM&;PRo=URR%>s+)Rk9jbK_9F1Tf<P;t}bUvq+UC)^TE~@YBfJFDvCY--`sMdHnk8}9t}$c zfZztyyviF+|EMDBRQrO(MeClAo)ujd*#bi9!HTOxkW-?8=0eYk7+41vdL`kYSwH7M z)j2R2f(y+t=D7^M1-}?>f>x4V0xUw)lL-#WVfp{*jWfgBv{fqO6SmdS*hsu%yTFjK zMX0QyK+(T@ulPRqusFRtN3OdqZD6h3bN5VKsY0XqYi`{9>(29PpaYv`t#iv0=;#B- z6Ri4B++<4P?K?Ng>hHEvEDt(rn8e-)+|EvWzF(0Bn%*VPW7M*bjZLt?NyQRcqhr5M zVLRHuq DJEDNK