Restructures frontend to a feature-based folder layout
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Skipped
Continuous Integration / backend-test (pull_request) Skipped
Continuous Integration / vulnerability-scan (pull_request) Skipped
Continuous Integration / frontend-prepare (pull_request) Successful in 1m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m12s
Continuous Integration / frontend-test (pull_request) Successful in 4m42s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m51s
Continuous Integration / deploy-test (pull_request) Skipped

Groups auth, setup, invitation, profile, users, cms, availability and
system code (services/hooks, components, schemas, mocks, pages) under
src/features/<name> instead of splitting by technical layer (api/,
components/, lib/schemas/, mocks/, pages/). Renames the old
connection-oriented `api` layer to `services` per feature, and splits
the monolithic api/types.ts into per-feature types.ts files (with
ProblemDetails/ApiResult merged into lib/api-client.ts as shared
infra). Layout-agnostic code (ui primitives, app shell, i18n, test
utils, lib) stays at the top level.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 22:30:37 +02:00
co-authored by Claude Sonnet 5
parent 650cf09cfa
commit 11ec08aac3
83 changed files with 249 additions and 254 deletions
@@ -0,0 +1,112 @@
import { useTranslation } from 'react-i18next';
import { MoreHorizontal, CheckCircle, XCircle, MinusCircle } from 'lucide-react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import type { CmsInstance, CmsInstanceStatus } from '@/features/cms/services/types';
const STATUS_BADGE_CONFIG: Record<
CmsInstanceStatus,
{ colorClass: string; Icon: React.ComponentType<{ className?: string }> }
> = {
Available: {
colorClass: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
Icon: CheckCircle,
},
NotAvailable: {
colorClass: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
Icon: XCircle,
},
Inactive: {
colorClass: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300',
Icon: MinusCircle,
},
};
function formatDate(iso: string | null): string {
if (!iso) return '—';
return new Date(iso).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
}
interface CmsInstanceListProps {
instances: CmsInstance[];
onSetStatus: (instance: CmsInstance) => void;
}
export function CmsInstanceList({ instances, onSetStatus }: CmsInstanceListProps) {
const { t } = useTranslation();
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('cms.table.name')}</TableHead>
<TableHead>{t('cms.table.url')}</TableHead>
<TableHead>{t('cms.table.status')}</TableHead>
<TableHead>{t('cms.table.lastContact')}</TableHead>
<TableHead>{t('cms.table.disableMessage')}</TableHead>
<TableHead>{t('cms.table.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{instances.map((instance) => {
const { colorClass, Icon } = STATUS_BADGE_CONFIG[instance.status];
return (
<TableRow
key={instance.id}
data-testid="cms-instance-row"
className={instance.status === 'Inactive' ? 'opacity-50' : ''}
>
<TableCell>{instance.name}</TableCell>
<TableCell>{instance.url}</TableCell>
<TableCell>
<div
data-testid="cms-instance-status-badge"
className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm font-medium ${colorClass}`}
>
<Icon className="size-4" />
<span>{t(`cms.status.${instance.status}`)}</span>
</div>
</TableCell>
<TableCell>{formatDate(instance.lastContactedAt)}</TableCell>
<TableCell>{instance.disableMessage ?? '—'}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
data-testid="cms-instance-actions-trigger"
>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
data-testid="cms-instance-set-status"
onClick={() => onSetStatus(instance)}
>
{t('cms.actions.setStatus')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}