diff --git a/src/components/common/ActiveStakesList.tsx b/src/components/common/ActiveStakesList.tsx index 81b77641..6d797361 100644 --- a/src/components/common/ActiveStakesList.tsx +++ b/src/components/common/ActiveStakesList.tsx @@ -53,7 +53,7 @@ function StakeRow({ stake, isPendingUnstake, onUnstake }: StakeRowProps) { const expired = useMemo(() => isExpired(stake.lockExpiresAt), [stake.lockExpiresAt]); const countdown = useMemo( () => (expired ? 'Unlocked' : formatCountdown(stake.lockExpiresAt)), - // eslint-disable-next-line react-hooks/exhaustive-deps + [expired, stake.lockExpiresAt] ); const lockDate = new Date(stake.lockExpiresAt).toLocaleDateString(undefined, { diff --git a/src/components/settings/SessionSecuritySection.tsx b/src/components/settings/SessionSecuritySection.tsx new file mode 100644 index 00000000..bc9cf8b7 --- /dev/null +++ b/src/components/settings/SessionSecuritySection.tsx @@ -0,0 +1,310 @@ +import { useState } from 'react'; +import { + Laptop, + Smartphone, + Monitor, + ShieldCheck, + Trash2, + LogOut, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { AsyncButton } from '@/components/ui/async-button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { cn } from '@/lib/utils'; +import type { Session } from '@/services/session.service'; +import { formatTimestampTooltip } from '@/utils/time.utils'; + +export interface SessionSecuritySectionProps { + sessions: Session[]; + isLoading?: boolean; + isError?: boolean; + isRevoking?: boolean; + onRevokeSession: (sessionId: string) => void; + onRevokeAllOtherSessions: () => void; + className?: string; +} + +function DeviceIcon({ device }: { device: string }) { + const label = device.toLowerCase(); + const className = 'size-5 text-white/60'; + if (label.includes('iphone') || label.includes('android') || label.includes('phone')) { + return