From e71a0cfb35dd10e4162cc78beb2832d1f7b54777 Mon Sep 17 00:00:00 2001 From: AndersonDesign1 Date: Sat, 6 Jun 2026 13:59:34 +0100 Subject: [PATCH 01/18] ci: add React Doctor GitHub Actions workflow --- .github/workflows/react-doctor.yml | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/react-doctor.yml diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml new file mode 100644 index 0000000..ec29b28 --- /dev/null +++ b/.github/workflows/react-doctor.yml @@ -0,0 +1,56 @@ +# React Doctor — finds security, performance, correctness, accessibility, +# bundle-size, and architecture issues in React codebases. +# +# Docs: https://www.react.doctor/ci +# Source: https://github.com/millionco/react-doctor + +name: React Doctor + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # Scans `main` on every push so you get a health-score trend on the + # default branch — useful for tracking the overall number commit-by-commit + # and catching regressions that slipped past PR review. PR-specific steps + # (the sticky summary comment) are skipped automatically on `push` events. + # Comment this block out if you only want PR-time scans. + push: + branches: [main] + +permissions: + # `actions/checkout` needs this to read the repo source. + contents: read + # Two uses: (1) reads the PR's changed-file list so the scan only checks + # what the PR touched (faster, scoped to the diff), and (2) posts/updates + # the sticky React Doctor summary comment on the PR. Downgrade `write` to + # `read` to keep the changed-file scan but disable comment posting. + pull-requests: write + # The sticky-comment step uses GitHub's `issues.createComment` / + # `issues.updateComment` endpoints — those are the same APIs that back PR + # comments (PRs are issues under the hood). Not exercised on `push` + # events, so safe to drop if you only run on `main`. + issues: write + +# Cancels any in-flight scan for the same PR (or branch, on push) the moment +# a new commit arrives, so reviewers only ever see the latest run. +concurrency: + group: react-doctor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + react-doctor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: millionco/react-doctor@v1 + # Common configuration knobs — uncomment any to override the default. + # Full reference: https://www.react.doctor/ci + # with: + # non-blocking: true # Report findings but always exit 0 (won't fail the PR check) + # fail-on: warning # Gate level: "error" (default) | "warning" | "none" + # comment: false # Disable the sticky PR summary comment + # annotations: false # Disable inline GitHub Actions annotations on changed files + # version: "0.2.18" # Pin to a specific react-doctor version instead of "latest" + # directory: apps/web # Scan a sub-directory (default: ".") + # project: "web,admin" # In a monorepo, scan specific workspace project(s) From 4cc262a0e6e5540160f7625d1cfc08acbc4784d0 Mon Sep 17 00:00:00 2001 From: AndersonDesign1 Date: Sun, 7 Jun 2026 14:44:11 +0100 Subject: [PATCH 02/18] refactor: add PanelSection and MutationErrorBanner common components --- src/components/common/panel-section.tsx | 34 +++++++++++++++++++++++++ src/components/common/state-panel.tsx | 14 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/components/common/panel-section.tsx diff --git a/src/components/common/panel-section.tsx b/src/components/common/panel-section.tsx new file mode 100644 index 0000000..11f4502 --- /dev/null +++ b/src/components/common/panel-section.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from "react"; + +interface PanelSectionProps { + title: string; + description?: string; + action?: ReactNode; + children: ReactNode; + className?: string; +} + +export function PanelSection({ + title, + description, + action, + children, + className, +}: PanelSectionProps) { + return ( +
+
+
+

{title}

+ {description && ( +

+ {description} +

+ )} +
+ {action} +
+ {children} +
+ ); +} diff --git a/src/components/common/state-panel.tsx b/src/components/common/state-panel.tsx index db5d72d..df5f9dc 100644 --- a/src/components/common/state-panel.tsx +++ b/src/components/common/state-panel.tsx @@ -54,3 +54,17 @@ export function EmptyPanel({ ); } + +export function MutationErrorBanner({ + error, +}: { + error: { message: string } | null | undefined; +}) { + if (!error) return null; + return ( +

+ {error.message} +

+ ); +} + From a3f36ece3f5daf668146fd2273df8f04e00dd04c Mon Sep 17 00:00:00 2001 From: AndersonDesign1 Date: Sun, 7 Jun 2026 14:44:36 +0100 Subject: [PATCH 03/18] refactor: clean up project-files-panel.tsx, use PanelSection and MutationErrorBanner, merge file icon utilities --- .../projects/project-files-panel.tsx | 76 +++++++------------ 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/src/components/projects/project-files-panel.tsx b/src/components/projects/project-files-panel.tsx index 8e24152..7f060bf 100644 --- a/src/components/projects/project-files-panel.tsx +++ b/src/components/projects/project-files-panel.tsx @@ -1,5 +1,3 @@ -"use client"; - import { Delete02Icon, Download01Icon, @@ -12,10 +10,12 @@ import { import { HugeiconsIcon } from "@hugeicons/react"; import { useQueryClient } from "@tanstack/react-query"; import { useRef, useState } from "react"; +import { PanelSection } from "@/components/common/panel-section"; import { EmptyPanel, ErrorPanel, LoadingPanel, + MutationErrorBanner, } from "@/components/common/state-panel"; import { Button } from "@/components/ui/button"; import { @@ -83,38 +83,21 @@ function getUploadValidationError(files: File[]) { return null; } -function getFileIcon(mimeType: string, fileName: string) { - const name = fileName.toLowerCase(); - const mime = mimeType.toLowerCase(); - - if (mime.startsWith("image/")) { - return FileImageIcon; - } - if (mime === "application/pdf" || name.endsWith(".pdf")) { - return Pdf01Icon; - } - if ( - mime.includes("spreadsheet") || - mime.includes("csv") || - mime.includes("excel") || - name.endsWith(".csv") || - name.endsWith(".xls") || - name.endsWith(".xlsx") - ) { - return FileChartColumnIcon; - } - return FileEmpty01Icon; -} - -function getFileIconColor(mimeType: string, fileName: string) { +function getFileTypeInfo(mimeType: string, fileName: string) { const name = fileName.toLowerCase(); const mime = mimeType.toLowerCase(); if (mime.startsWith("image/")) { - return "text-purple-600 bg-purple-50 dark:bg-purple-950/20 border-purple-200 dark:border-purple-900"; + return { + icon: FileImageIcon, + colorStyles: "text-purple-600 bg-purple-50 dark:bg-purple-950/20 border-purple-200 dark:border-purple-900", + }; } if (mime === "application/pdf" || name.endsWith(".pdf")) { - return "text-rose-600 bg-rose-50 dark:bg-rose-950/20 border-rose-200 dark:border-rose-900"; + return { + icon: Pdf01Icon, + colorStyles: "text-rose-600 bg-rose-50 dark:bg-rose-950/20 border-rose-200 dark:border-rose-900", + }; } if ( mime.includes("spreadsheet") || @@ -124,9 +107,15 @@ function getFileIconColor(mimeType: string, fileName: string) { name.endsWith(".xls") || name.endsWith(".xlsx") ) { - return "text-emerald-600 bg-emerald-50 dark:bg-emerald-950/20 border-emerald-200 dark:border-emerald-900"; + return { + icon: FileChartColumnIcon, + colorStyles: "text-emerald-600 bg-emerald-50 dark:bg-emerald-950/20 border-emerald-200 dark:border-emerald-900", + }; } - return "text-teal-600 bg-teal-50 dark:bg-teal-950/20 border-teal-200 dark:border-teal-900"; + return { + icon: FileEmpty01Icon, + colorStyles: "text-teal-600 bg-teal-50 dark:bg-teal-950/20 border-teal-200 dark:border-teal-900", + }; } interface ProjectFilesPanelProps { @@ -218,18 +207,10 @@ export function ProjectFilesPanel({ Boolean(filesQuery.error && visibleFiles.length === 0); return ( -
- {/* Panel Header */} -
-
-

- Project Files -

-

- Share and retrieve assets, templates, or documents securely. -

-
-
+ - {mutationError ? ( -
- {mutationError} -
- ) : null} + {/* Loading & Error States */} {filesQuery.isLoading && visibleFiles.length === 0 ? ( @@ -316,8 +293,7 @@ export function ProjectFilesPanel({ {visibleFiles.map((file) => { - const IconComponent = getFileIcon(file.mimeType, file.fileName); - const colorStyles = getFileIconColor( + const { icon: IconComponent, colorStyles } = getFileTypeInfo( file.mimeType, file.fileName ); @@ -389,6 +365,6 @@ export function ProjectFilesPanel({ ) : null} -
+ ); } From ab9617b33e4bcf223da47a8807765521bc825244 Mon Sep 17 00:00:00 2001 From: AndersonDesign1 Date: Sun, 7 Jun 2026 14:44:55 +0100 Subject: [PATCH 04/18] refactor: clean up project-milestones-panel.tsx, use PanelSection, MutationErrorBanner, and useCrudDialogs hook --- .../projects/project-milestones-panel.tsx | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/components/projects/project-milestones-panel.tsx b/src/components/projects/project-milestones-panel.tsx index 298cb21..46f6fc1 100644 --- a/src/components/projects/project-milestones-panel.tsx +++ b/src/components/projects/project-milestones-panel.tsx @@ -5,10 +5,12 @@ import { } from "@hugeicons/core-free-icons"; import { type FormEvent, useState } from "react"; import { UnifiedActivityList } from "@/components/common/activity-list"; +import { PanelSection } from "@/components/common/panel-section"; import { EmptyPanel, ErrorPanel, LoadingPanel, + MutationErrorBanner, } from "@/components/common/state-panel"; import { Button } from "@/components/ui/button"; import { @@ -44,6 +46,7 @@ import { useProjectMilestonesData, useUpdateProjectMilestoneMutation, } from "@/lib/api"; +import { optionalString } from "@/lib/utils"; const MILESTONE_STATUS_OPTIONS = [ { label: "To do", value: "todo" }, @@ -75,10 +78,6 @@ function toMilestonePayload( }; } -function optionalString(value: string) { - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} export function formatMilestoneStatus(status: ProjectMilestoneStatus) { return ( @@ -335,6 +334,17 @@ function ProjectMilestoneForm({ ); } +function useCrudDialogs() { + const [isAddOpen, setIsAddOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + return { + isAddOpen, + setIsAddOpen, + editingItem, + setEditingItem, + }; +} + export function ProjectMilestonesPanel({ canManage, projectId, @@ -346,9 +356,12 @@ export function ProjectMilestonesPanel({ const createMilestone = useCreateProjectMilestoneMutation(); const updateMilestone = useUpdateProjectMilestoneMutation(); const deleteMilestone = useDeleteProjectMilestoneMutation(); - const [isAddOpen, setIsAddOpen] = useState(false); - const [editingMilestone, setEditingMilestone] = - useState(null); + const { + isAddOpen, + setIsAddOpen, + editingItem: editingMilestone, + setEditingItem: setEditingMilestone, + } = useCrudDialogs(); if (milestonesQuery.isLoading && !milestonesQuery.data) { return ( @@ -368,17 +381,9 @@ export function ProjectMilestonesPanel({ const milestones = milestonesQuery.data ?? []; return ( -
-
-
-

- Project Milestones -

-

- Track key deliverables and deadlines for maximum transparency. -

-
- {canManage && ( + setIsAddOpen(true)} @@ -386,9 +391,11 @@ export function ProjectMilestonesPanel({ > Add Milestone - )} -
- + ) + } + description="Track key deliverables and deadlines for maximum transparency." + title="Project Milestones" + > {/* Add Milestone Dialog */} {canManage && ( @@ -462,11 +469,7 @@ export function ProjectMilestonesPanel({ }} onEdit={setEditingMilestone} /> - {deleteMilestone.error ? ( -

- {deleteMilestone.error.message} -

- ) : null} -
+ + ); } From 891ac39a0461a79c32f7036f2770f89644502233 Mon Sep 17 00:00:00 2001 From: AndersonDesign1 Date: Sun, 7 Jun 2026 14:45:02 +0100 Subject: [PATCH 05/18] refactor: move optionalString helper to lib/utils.ts --- src/lib/utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 88283f0..3918522 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,3 +5,9 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function optionalString(value: string) { + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + From 11966e02b2879da4f21e9dace0d60011aee0d9c0 Mon Sep 17 00:00:00 2001 From: AndersonDesign1 Date: Sun, 7 Jun 2026 14:45:27 +0100 Subject: [PATCH 06/18] refactor: clean up project-updates-panel.tsx to use PanelSection, MutationErrorBanner, standard UI fields, and useCrudDialogs hook --- .../projects/project-updates-panel.tsx | 191 +++++++++++------- 1 file changed, 113 insertions(+), 78 deletions(-) diff --git a/src/components/projects/project-updates-panel.tsx b/src/components/projects/project-updates-panel.tsx index 106c0bb..23d3f84 100644 --- a/src/components/projects/project-updates-panel.tsx +++ b/src/components/projects/project-updates-panel.tsx @@ -1,9 +1,11 @@ import { type FormEvent, useState } from "react"; import { UnifiedActivityList } from "@/components/common/activity-list"; +import { PanelSection } from "@/components/common/panel-section"; import { EmptyPanel, ErrorPanel, LoadingPanel, + MutationErrorBanner, } from "@/components/common/state-panel"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -15,6 +17,22 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; import { type ProjectUpdate, type ProjectUpdatePayload, @@ -26,6 +44,7 @@ import { } from "@/lib/api"; import { cn } from "@/lib/utils"; + const UPDATE_STATUS_OPTIONS = [ { label: "On track", value: "on_track" }, { label: "At risk", value: "at_risk" }, @@ -201,6 +220,11 @@ function ProjectUpdateForm({ } } + const statusItems = UPDATE_STATUS_OPTIONS.map((option) => ({ + label: option.label, + value: option.value, + })); + return (
undefined); }} > -
-