215 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|