life-manager/codebase/features/projects/frontend/ProjectLayout.tsx
2026-03-17 17:51:06 -07:00

215 lines
5.6 KiB
TypeScript

/** @jsxImportSource react */
import { lazy, Suspense, useState } from 'react';
import type { ReactElement } from 'react';
import { NavLink, Outlet, useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { useTheme } from '@lilith/ui-theme';
import { ProjectType } from '@life-platform/shared';
import { useProjectBySlug } from './useProjectsCrud';
import { PROJECT_TYPE_REGISTRY } from './type-registry';
import type { QuickActionDef } from './type-registry';
import { Icon } from '@lilith/ui-icons';
import { ProjectContextValue } from './useProjectContext';
const QuickLogModal = lazy(() => import('./modals/QuickLogModal'));
const Layout = styled.div`
display: flex;
flex-direction: column;
min-height: 100%;
`;
const ColorStripe = styled.div<{ color: string }>`
height: 4px;
background: ${({ color }) => color};
box-shadow: 0 0 16px ${({ color }) => color}44;
border-radius: 2px;
margin-bottom: 16px;
`;
const Header = styled.div`
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
`;
const HeaderLeft = styled.div`
display: flex;
align-items: center;
gap: 10px;
flex: 1;
`;
const QuickActions = styled.div`
display: flex;
gap: 6px;
`;
const QuickActionButton = styled.button<{ accent: string }>`
display: flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 4px;
border: 1px solid ${({ accent }) => `${accent}44`};
background: ${({ accent }) => `${accent}11`};
color: ${({ accent }) => accent};
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
&:hover {
background: ${({ accent }) => `${accent}22`};
border-color: ${({ accent }) => accent};
}
`;
const ProjectIcon = styled.span`
font-size: 20px;
`;
const ProjectName = styled.h2`
font-size: 18px;
font-weight: 700;
color: ${({ theme }) => theme.colors.text.primary};
margin: 0;
font-family: monospace;
`;
const SubNav = styled.nav`
display: flex;
gap: 0;
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
margin-bottom: 20px;
overflow-x: auto;
&::-webkit-scrollbar {
height: 0;
}
`;
const SubNavLink = styled(NavLink)<{ $accent: string }>`
padding: 8px 14px;
font-size: 12px;
color: ${({ theme }) => theme.colors.text.muted};
text-decoration: none;
white-space: nowrap;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.15s;
&:hover {
color: ${({ theme }) => theme.colors.text.primary};
}
&.active {
color: ${({ $accent }) => $accent};
border-bottom-color: ${({ $accent }) => $accent};
}
`;
export function ProjectLayout(): ReactElement {
const { slug: domainSlug, categorySlug, projectSlug } = useParams<{
slug: string;
categorySlug: string;
projectSlug: string;
}>();
const { data: project, isLoading } = useProjectBySlug(projectSlug);
const navigate = useNavigate();
const { theme } = useTheme();
const [activeModal, setActiveModal] = useState<string | null>(null);
if (isLoading) {
return (
<Layout>
<p style={{ color: theme.colors.text.muted, fontSize: '13px' }}>Loading project...</p>
</Layout>
);
}
if (!project) {
return (
<Layout>
<p style={{ color: theme.colors.error.text, fontSize: '13px' }}>
Project not found: {projectSlug}
</p>
</Layout>
);
}
const color = project.color || theme.colors.primary.main;
const basePath = `/domains/${domainSlug}/${categorySlug}/${projectSlug}`;
const config = PROJECT_TYPE_REGISTRY[project.type];
const tabs = config?.tabs ?? [];
const HeaderWidget = config?.HeaderWidget;
const quickActions = config?.quickActions;
function handleQuickAction(action: QuickActionDef): void {
if (action.action === 'navigate') {
navigate(`${basePath}/${action.target}`);
} else {
setActiveModal(action.target);
}
}
return (
<ProjectContextValue.Provider value={{ project }}>
<Layout>
<ColorStripe color={color} />
<Header>
<HeaderLeft>
{project.icon && <ProjectIcon><Icon name={project.icon} size={20} /></ProjectIcon>}
<ProjectName>{project.name}</ProjectName>
</HeaderLeft>
{quickActions && quickActions.length > 0 && (
<QuickActions>
{quickActions.map((qa) => (
<QuickActionButton
key={qa.target}
accent={color}
onClick={() => handleQuickAction(qa)}
>
{qa.icon && <Icon name={qa.icon} size={16} />}
{qa.label}
</QuickActionButton>
))}
</QuickActions>
)}
</Header>
{HeaderWidget && (
<Suspense fallback={null}>
<HeaderWidget />
</Suspense>
)}
<SubNav aria-label={`${project.name} navigation`}>
<SubNavLink to={basePath} end $accent={color}>
Dashboard
</SubNavLink>
{tabs.map((tab) => (
<SubNavLink key={tab.slug} to={`${basePath}/${tab.slug}`} $accent={color}>
{tab.label}
</SubNavLink>
))}
</SubNav>
<Outlet />
{project.type === ProjectType.Fitness && (
<Suspense fallback={null}>
<QuickLogModal
open={activeModal === 'quick-log'}
onClose={() => setActiveModal(null)}
projectId={project.id}
accent={color}
/>
</Suspense>
)}
</Layout>
</ProjectContextValue.Provider>
);
}