Overhaul UI with shadcn components and new pages

Add shadcn/ui components (badge, button, card, input, select,
separator). Add company context provider. New pages: Activity,
Approvals, Companies, Costs, Org chart. Restyle existing pages
(Dashboard, Agents, Issues, Goals, Projects) with shadcn components
and dark theme. Update layout, sidebar navigation, and routing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-17 09:07:32 -06:00
parent abadd469bc
commit 22e7930d0b
40 changed files with 1555 additions and 137 deletions

View File

@@ -0,0 +1,124 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import type { Company } from "@paperclip/shared";
import { companiesApi } from "../api/companies";
interface CompanyContextValue {
companies: Company[];
selectedCompanyId: string | null;
selectedCompany: Company | null;
loading: boolean;
error: Error | null;
setSelectedCompanyId: (companyId: string) => void;
reloadCompanies: () => Promise<void>;
createCompany: (data: {
name: string;
description?: string | null;
budgetMonthlyCents?: number;
}) => Promise<Company>;
}
const STORAGE_KEY = "paperclip.selectedCompanyId";
const CompanyContext = createContext<CompanyContextValue | null>(null);
export function CompanyProvider({ children }: { children: ReactNode }) {
const [companies, setCompanies] = useState<Company[]>([]);
const [selectedCompanyId, setSelectedCompanyIdState] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const setSelectedCompanyId = useCallback((companyId: string) => {
setSelectedCompanyIdState(companyId);
localStorage.setItem(STORAGE_KEY, companyId);
}, []);
const reloadCompanies = useCallback(async () => {
setLoading(true);
setError(null);
try {
const rows = await companiesApi.list();
setCompanies(rows);
if (rows.length === 0) {
setSelectedCompanyIdState(null);
return;
}
const stored = localStorage.getItem(STORAGE_KEY);
const next = rows.some((company) => company.id === stored)
? stored
: selectedCompanyId && rows.some((company) => company.id === selectedCompanyId)
? selectedCompanyId
: rows[0]!.id;
if (next) {
setSelectedCompanyIdState(next);
localStorage.setItem(STORAGE_KEY, next);
}
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to load companies"));
} finally {
setLoading(false);
}
}, [selectedCompanyId]);
useEffect(() => {
void reloadCompanies();
}, [reloadCompanies]);
const createCompany = useCallback(
async (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => {
const company = await companiesApi.create(data);
await reloadCompanies();
setSelectedCompanyId(company.id);
return company;
},
[reloadCompanies, setSelectedCompanyId],
);
const selectedCompany = useMemo(
() => companies.find((company) => company.id === selectedCompanyId) ?? null,
[companies, selectedCompanyId],
);
const value = useMemo(
() => ({
companies,
selectedCompanyId,
selectedCompany,
loading,
error,
setSelectedCompanyId,
reloadCompanies,
createCompany,
}),
[
companies,
selectedCompanyId,
selectedCompany,
loading,
error,
setSelectedCompanyId,
reloadCompanies,
createCompany,
],
);
return <CompanyContext.Provider value={value}>{children}</CompanyContext.Provider>;
}
export function useCompany() {
const ctx = useContext(CompanyContext);
if (!ctx) {
throw new Error("useCompany must be used within CompanyProvider");
}
return ctx;
}