app now responsive for mobile and small devices.
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
'use client';
|
||||
|
||||
import { Check, History, Trash2, UserPlus, X } from 'lucide-react';
|
||||
import type {
|
||||
CounterpartItemDto,
|
||||
CounterpartSearchResultDto,
|
||||
} from '@/lib/api/organization';
|
||||
import { Badge, organizationConnectionStatusVariant } from '@/components/ui/shared/Badge';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Card } from '@/components/ui/shared/Card';
|
||||
import { Input } from '@/components/ui/shared/Input';
|
||||
import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton';
|
||||
import type { InvitationLinkTarget } from '@/components/invitations/organizationInviteLinks';
|
||||
|
||||
type OrganizationConnectionsMobileListProps = {
|
||||
loading: boolean;
|
||||
mode: 'existing' | 'search';
|
||||
existingRows: CounterpartItemDto[];
|
||||
searchResults: CounterpartSearchResultDto[];
|
||||
currentOrganizationId: string;
|
||||
counterpart: string;
|
||||
pendingConnectionRowId: string | null;
|
||||
deleteConnectionRowId: string | null;
|
||||
copiedId: string | null;
|
||||
copyingInvitationId: string | null;
|
||||
showInviteForm: boolean;
|
||||
manualOrganizationName: string;
|
||||
manualOwnerEmail: string;
|
||||
inviteLoading: boolean;
|
||||
formatConnectionStatusLabel: (row: CounterpartItemDto, orgId: string) => string;
|
||||
formatTableDate: (value: string) => string;
|
||||
getInvitationTarget: (row: CounterpartItemDto) => InvitationLinkTarget | null;
|
||||
onCopyInvitation: (row: CounterpartItemDto) => void;
|
||||
onRespond: (rowId: string, action: 'ACCEPT' | 'REJECT') => void;
|
||||
onViewCaseHistory: (row: CounterpartItemDto) => void;
|
||||
onDeleteConnection: (rowId: string) => void;
|
||||
onSendConnectionRequest: (orgId: string) => void;
|
||||
onToggleInviteForm: () => void;
|
||||
onManualOrganizationNameChange: (value: string) => void;
|
||||
onManualOwnerEmailChange: (value: string) => void;
|
||||
onSendInvite: () => void;
|
||||
labels: {
|
||||
loading: string;
|
||||
emptyConnections: string;
|
||||
noDirectoryResults: string;
|
||||
hideInvitationFields: string;
|
||||
sendInvitationLink: string;
|
||||
counterpartNameLabel: string;
|
||||
ownerEmailLabel: string;
|
||||
sendInvitation: string;
|
||||
sendRequest: string;
|
||||
acceptRequest: string;
|
||||
declineRequest: string;
|
||||
viewCaseHistory: string;
|
||||
removeConnection: string;
|
||||
statusToday: string;
|
||||
statusFound: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function OrganizationConnectionsMobileList({
|
||||
loading,
|
||||
mode,
|
||||
existingRows,
|
||||
searchResults,
|
||||
currentOrganizationId,
|
||||
counterpart,
|
||||
pendingConnectionRowId,
|
||||
deleteConnectionRowId,
|
||||
copiedId,
|
||||
copyingInvitationId,
|
||||
showInviteForm,
|
||||
manualOrganizationName,
|
||||
manualOwnerEmail,
|
||||
inviteLoading,
|
||||
formatConnectionStatusLabel,
|
||||
formatTableDate,
|
||||
getInvitationTarget,
|
||||
onCopyInvitation,
|
||||
onRespond,
|
||||
onViewCaseHistory,
|
||||
onDeleteConnection,
|
||||
onSendConnectionRequest,
|
||||
onToggleInviteForm,
|
||||
onManualOrganizationNameChange,
|
||||
onManualOwnerEmailChange,
|
||||
onSendInvite,
|
||||
labels,
|
||||
}: OrganizationConnectionsMobileListProps) {
|
||||
if (loading) {
|
||||
return <p className="text-sm text-text-secondary lg:hidden">{labels.loading}</p>;
|
||||
}
|
||||
|
||||
if (mode === 'existing') {
|
||||
if (existingRows.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-text-secondary lg:hidden surface-card p-4">
|
||||
{labels.emptyConnections}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-3 lg:hidden">
|
||||
{existingRows.map((row) => {
|
||||
const canRespond =
|
||||
row.status === 'PENDING' &&
|
||||
row.requestedByOrganizationId !== null &&
|
||||
row.requestedByOrganizationId !== currentOrganizationId;
|
||||
const invitationTarget = getInvitationTarget(row);
|
||||
|
||||
return (
|
||||
<li key={row.id}>
|
||||
<Card padding="sm" className="space-y-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-text-primary truncate">{row.organizationName}</p>
|
||||
<p className="text-sm text-text-secondary truncate mt-0.5">{row.ownerEmail}</p>
|
||||
<p className="text-xs text-text-muted mt-1">{formatTableDate(row.createdAt)}</p>
|
||||
</div>
|
||||
<Badge variant={organizationConnectionStatusVariant(row.status)} fixedWidth={false}>
|
||||
{formatConnectionStatusLabel(row, currentOrganizationId)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1 border-t border-border/60">
|
||||
{invitationTarget ? (
|
||||
<CopyInvitationLinkButton
|
||||
invitation={invitationTarget}
|
||||
copied={copiedId === invitationTarget.id}
|
||||
copying={copyingInvitationId === invitationTarget.id}
|
||||
onCopy={() => onCopyInvitation(row)}
|
||||
/>
|
||||
) : null}
|
||||
{canRespond ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
||||
disabled={pendingConnectionRowId !== null && pendingConnectionRowId !== row.id}
|
||||
onClick={() => onRespond(row.id, 'ACCEPT')}
|
||||
aria-label={labels.acceptRequest}
|
||||
title={labels.acceptRequest}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-red-500/15 hover:text-red-600 disabled:opacity-50"
|
||||
disabled={pendingConnectionRowId !== null && pendingConnectionRowId !== row.id}
|
||||
onClick={() => onRespond(row.id, 'REJECT')}
|
||||
aria-label={labels.declineRequest}
|
||||
title={labels.declineRequest}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{row.status === 'ACTIVE' ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary"
|
||||
onClick={() => onViewCaseHistory(row)}
|
||||
aria-label={labels.viewCaseHistory}
|
||||
title={labels.viewCaseHistory}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-red-500/15 hover:text-red-600 disabled:opacity-50"
|
||||
disabled={deleteConnectionRowId !== null && deleteConnectionRowId !== row.id}
|
||||
onClick={() => onDeleteConnection(row.id)}
|
||||
aria-label={labels.removeConnection}
|
||||
title={labels.removeConnection}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
if (searchResults.length > 0) {
|
||||
return (
|
||||
<ul className="space-y-3 lg:hidden">
|
||||
{searchResults.map((result) => (
|
||||
<li key={result.id}>
|
||||
<Card padding="sm" className="space-y-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-text-primary truncate">{result.name}</p>
|
||||
<p className="text-sm text-text-secondary truncate mt-0.5">{result.owner.email}</p>
|
||||
<p className="text-xs text-text-muted mt-1">{labels.statusToday}</p>
|
||||
</div>
|
||||
<Badge variant="default" fixedWidth={false}>
|
||||
{labels.statusFound}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-end pt-1 border-t border-border/60">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
||||
disabled={pendingConnectionRowId !== null && pendingConnectionRowId !== result.id}
|
||||
onClick={() => onSendConnectionRequest(result.id)}
|
||||
aria-label={labels.sendRequest}
|
||||
title={labels.sendRequest}
|
||||
>
|
||||
<UserPlus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lg:hidden surface-card p-4 space-y-3">
|
||||
<p className="text-sm text-text-secondary">{labels.noDirectoryResults}</p>
|
||||
<Button type="button" onClick={onToggleInviteForm} className="w-full sm:w-auto">
|
||||
{showInviteForm ? labels.hideInvitationFields : labels.sendInvitationLink}
|
||||
</Button>
|
||||
{showInviteForm ? (
|
||||
<div className="grid gap-3">
|
||||
<Input
|
||||
label={labels.counterpartNameLabel}
|
||||
value={manualOrganizationName}
|
||||
onChange={(e) => onManualOrganizationNameChange(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={labels.ownerEmailLabel}
|
||||
type="email"
|
||||
value={manualOwnerEmail}
|
||||
onChange={(e) => onManualOwnerEmailChange(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
isLoading={inviteLoading}
|
||||
disabled={!manualOrganizationName.trim() || !manualOwnerEmail.trim()}
|
||||
onClick={onSendInvite}
|
||||
className="w-full"
|
||||
>
|
||||
{labels.sendInvitation}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user