app now responsive for mobile and small devices.
This commit is contained in:
190
frontend/src/components/staff/StaffMembersMobileList.tsx
Normal file
190
frontend/src/components/staff/StaffMembersMobileList.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import { Check, Copy, Pencil, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||
import type { StaffMemberDto } from '@/lib/api/staff';
|
||||
import { Badge } from '@/components/ui/shared/Badge';
|
||||
import { Card } from '@/components/ui/shared/Card';
|
||||
|
||||
type StaffMembersMobileListProps = {
|
||||
members: StaffMemberDto[];
|
||||
canEdit: boolean;
|
||||
organizationType: string | undefined;
|
||||
copiedInviteMembershipId: string | null;
|
||||
copyingInviteMembershipId: string | null;
|
||||
enablingMembershipId: string | null;
|
||||
disablingMembershipId: string | null;
|
||||
formatAccessSummary: (member: StaffMemberDto) => string;
|
||||
canShareInviteLink: (member: StaffMemberDto) => boolean;
|
||||
canEnable: (member: StaffMemberDto) => boolean;
|
||||
canDisable: (member: StaffMemberDto) => boolean;
|
||||
onCopyInviteLink: (member: StaffMemberDto) => void;
|
||||
onEnable: (member: StaffMemberDto) => void;
|
||||
onDisable: (member: StaffMemberDto) => void;
|
||||
onEdit: (member: StaffMemberDto) => void;
|
||||
onDelete: (member: StaffMemberDto) => void;
|
||||
labels: {
|
||||
roleOwner: string;
|
||||
roleStaff: string;
|
||||
statusActive: string;
|
||||
statusPending: string;
|
||||
statusDisabled: string;
|
||||
statusExpired: string;
|
||||
allFeatures: string;
|
||||
copyInviteLink: string;
|
||||
enableMemberTitle: string;
|
||||
disableMemberTitle: string;
|
||||
editMemberAria: string;
|
||||
deleteMemberAria: string;
|
||||
};
|
||||
};
|
||||
|
||||
function memberStatusBadge(
|
||||
member: StaffMemberDto,
|
||||
labels: StaffMembersMobileListProps['labels'],
|
||||
) {
|
||||
if (member.isOwner || member.invitationStatus === 'ACTIVE') {
|
||||
return <Badge variant="success">{labels.statusActive}</Badge>;
|
||||
}
|
||||
if (member.invitationStatus === 'PENDING') {
|
||||
return <Badge variant="warning">{labels.statusPending}</Badge>;
|
||||
}
|
||||
if (member.invitationStatus === 'DISABLED') {
|
||||
return <Badge variant="default">{labels.statusDisabled}</Badge>;
|
||||
}
|
||||
return <Badge variant="danger">{labels.statusExpired}</Badge>;
|
||||
}
|
||||
|
||||
export function StaffMembersMobileList({
|
||||
members,
|
||||
canEdit,
|
||||
copiedInviteMembershipId,
|
||||
copyingInviteMembershipId,
|
||||
enablingMembershipId,
|
||||
disablingMembershipId,
|
||||
formatAccessSummary,
|
||||
canShareInviteLink,
|
||||
canEnable,
|
||||
canDisable,
|
||||
onCopyInviteLink,
|
||||
onEnable,
|
||||
onDisable,
|
||||
onEdit,
|
||||
onDelete,
|
||||
labels,
|
||||
}: StaffMembersMobileListProps) {
|
||||
return (
|
||||
<ul className="space-y-3 lg:hidden">
|
||||
{members.map((member) => (
|
||||
<li key={member.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">{member.name}</p>
|
||||
<p className="text-sm text-text-secondary truncate mt-0.5">{member.email}</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{member.isOwner ? labels.roleOwner : labels.roleStaff}
|
||||
</p>
|
||||
</div>
|
||||
{memberStatusBadge(member, labels)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-secondary line-clamp-3">
|
||||
{member.isOwner ? labels.allFeatures : formatAccessSummary(member)}
|
||||
</p>
|
||||
|
||||
{!member.isOwner ? (
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1 border-t border-border/60">
|
||||
{canShareInviteLink(member) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
||||
disabled={copyingInviteMembershipId === member.id}
|
||||
onClick={() => onCopyInviteLink(member)}
|
||||
aria-label={labels.copyInviteLink}
|
||||
title={labels.copyInviteLink}
|
||||
>
|
||||
{copiedInviteMembershipId === member.id ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
{canEnable(member) ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`p-2 rounded-md ${
|
||||
canEdit
|
||||
? 'text-text-secondary hover:bg-background-card/80 hover:text-primary'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label={labels.enableMemberTitle}
|
||||
disabled={!canEdit || enablingMembershipId === member.id}
|
||||
title={labels.enableMemberTitle}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
onEnable(member);
|
||||
}}
|
||||
>
|
||||
<UserCheck className="w-4 h-4" />
|
||||
</button>
|
||||
) : null}
|
||||
{canDisable(member) ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`p-2 rounded-md ${
|
||||
canEdit
|
||||
? 'text-text-secondary hover:bg-background-card/80 hover:text-amber-600'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label={labels.disableMemberTitle}
|
||||
disabled={!canEdit || disablingMembershipId === member.id}
|
||||
title={labels.disableMemberTitle}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
onDisable(member);
|
||||
}}
|
||||
>
|
||||
<UserX className="w-4 h-4" />
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={`p-2 rounded-md ${
|
||||
canEdit
|
||||
? 'text-text-secondary hover:bg-background-card/80 hover:text-text-primary'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label={labels.editMemberAria}
|
||||
disabled={!canEdit}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
onEdit(member);
|
||||
}}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`p-2 rounded-md ${
|
||||
canEdit
|
||||
? 'text-text-secondary hover:bg-red-500/15 hover:text-red-600'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label={labels.deleteMemberAria}
|
||||
disabled={!canEdit}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
onDelete(member);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -82,8 +82,8 @@ export function WorkingHoursEditor({
|
||||
key={day.dayOfWeek}
|
||||
className="rounded-[var(--radius-md)] border border-border/60 bg-background-card/40 px-3 py-3 space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-medium text-text-primary w-10">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="text-sm font-medium text-text-primary w-10 shrink-0">
|
||||
{t(WEEKDAY_KEYS[day.dayOfWeek])}
|
||||
</span>
|
||||
<Checkbox
|
||||
|
||||
Reference in New Issue
Block a user