initial commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
218
src/admin/components/dashboard/DashSessions.tsx
Normal file
218
src/admin/components/dashboard/DashSessions.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useAlert } from '../../context/AlertContext'
|
||||
import ConfirmModal from '../ConfirmModal'
|
||||
import useModalLock from '../../hooks/useModalLock'
|
||||
import apiFetch from '../../utils/api'
|
||||
import { formatSessionDate } from '../../utils/dashboardHelpers'
|
||||
|
||||
const API_BASE = '/api/admin'
|
||||
|
||||
interface DeviceInfo {
|
||||
icon?: string
|
||||
browser?: string
|
||||
os?: string
|
||||
}
|
||||
|
||||
interface Session {
|
||||
id: number | string
|
||||
is_current: boolean
|
||||
device_info?: DeviceInfo
|
||||
ip_address: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface DeleteModalState {
|
||||
isOpen: boolean
|
||||
session: Session | null
|
||||
}
|
||||
|
||||
function getDeviceIcon(iconType?: string) {
|
||||
switch (iconType) {
|
||||
case 'smartphone':
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
||||
</svg>
|
||||
)
|
||||
case 'tablet':
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" /><line x1="12" y1="18" x2="12" y2="18" />
|
||||
</svg>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function DashSessions() {
|
||||
const alert = useAlert()
|
||||
|
||||
const [sessions, setSessions] = useState<Session[]>([])
|
||||
const [sessionsLoading, setSessionsLoading] = useState(true)
|
||||
const [deleteModal, setDeleteModal] = useState<DeleteModalState>({ isOpen: false, session: null })
|
||||
const [deleteAllModal, setDeleteAllModal] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
useModalLock(deleteAllModal)
|
||||
|
||||
const fetchSessions = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/sessions`)
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setSessions(Array.isArray(data.data) ? data.data : data.data?.sessions || [])
|
||||
}
|
||||
} catch {
|
||||
// session fetch failed silently
|
||||
} finally {
|
||||
setSessionsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions()
|
||||
}, [fetchSessions])
|
||||
|
||||
const handleDeleteSession = async () => {
|
||||
if (!deleteModal.session) {
|
||||
return
|
||||
}
|
||||
const sessionId = deleteModal.session.id
|
||||
setDeleting(true)
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/sessions/${sessionId}`, { method: 'DELETE' })
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setDeleteModal({ isOpen: false, session: null })
|
||||
setSessions(prev => prev.filter(s => s.id !== sessionId))
|
||||
alert.success('Relace byla ukončena')
|
||||
} else {
|
||||
alert.error(data.error || 'Nepodařilo se ukončit relaci')
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAllSessions = async () => {
|
||||
setDeleting(true)
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/sessions?action=all`, { method: 'DELETE' })
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setDeleteAllModal(false)
|
||||
setSessions(prev => prev.filter(s => s.is_current))
|
||||
alert.success(data.message || 'Ostatní relace byly ukončeny')
|
||||
} else {
|
||||
alert.error(data.error || 'Nepodařilo se ukončit relace')
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="admin-card"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.15 }}
|
||||
>
|
||||
<div className="admin-card-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.75rem' }}>
|
||||
<h2 className="admin-card-title">Přihlášená zařízení</h2>
|
||||
{sessions.filter(s => !s.is_current).length > 0 && (
|
||||
<button onClick={() => setDeleteAllModal(true)} className="admin-btn admin-btn-secondary admin-btn-sm">
|
||||
Odhlásit ostatní
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||
{sessionsLoading && (
|
||||
<div className="admin-skeleton" style={{ padding: '1rem', gap: '1rem' }}>
|
||||
{[0, 1, 2].map(i => (
|
||||
<div key={i} className="admin-skeleton-row">
|
||||
<div className="admin-skeleton-line circle" />
|
||||
<div className="flex-1">
|
||||
<div className="admin-skeleton-line w-1/2" style={{ marginBottom: '0.5rem' }} />
|
||||
<div className="admin-skeleton-line w-1/3" style={{ height: '10px' }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!sessionsLoading && sessions.length === 0 && (
|
||||
<div className="text-secondary" style={{ padding: '1.5rem', textAlign: 'center', fontSize: '0.875rem' }}>
|
||||
Žádné aktivní relace
|
||||
</div>
|
||||
)}
|
||||
{!sessionsLoading && sessions.length > 0 && (
|
||||
<div className="sessions-list">
|
||||
{sessions.map((session) => (
|
||||
<div key={session.id} className={`session-item ${session.is_current ? 'session-item-current' : ''}`}>
|
||||
<div className="session-icon">{getDeviceIcon(session.device_info?.icon)}</div>
|
||||
<div className="session-info">
|
||||
<div className="session-device">
|
||||
{session.device_info?.browser} na {session.device_info?.os}
|
||||
{session.is_current && (
|
||||
<span className="admin-badge admin-badge-success" style={{ marginLeft: '0.5rem' }}>Aktuální</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="session-meta">
|
||||
<span>{session.ip_address}</span>
|
||||
<span className="session-meta-separator">|</span>
|
||||
<span>{formatSessionDate(session.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="session-actions">
|
||||
{!session.is_current && (
|
||||
<button onClick={() => setDeleteModal({ isOpen: true, session })} className="admin-btn-icon danger" title="Ukončit relaci" aria-label="Ukončit relaci">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={deleteModal.isOpen}
|
||||
onClose={() => setDeleteModal({ isOpen: false, session: null })}
|
||||
onConfirm={handleDeleteSession}
|
||||
title="Ukončit relaci"
|
||||
message={`Opravdu chcete ukončit relaci na zařízení "${deleteModal.session?.device_info?.browser} na ${deleteModal.session?.device_info?.os}"? Toto zařízení bude odhlášeno.`}
|
||||
confirmText="Ukončit"
|
||||
cancelText="Zrušit"
|
||||
type="danger"
|
||||
loading={deleting}
|
||||
/>
|
||||
<ConfirmModal
|
||||
isOpen={deleteAllModal}
|
||||
onClose={() => setDeleteAllModal(false)}
|
||||
onConfirm={handleDeleteAllSessions}
|
||||
title="Odhlásit ostatní zařízení"
|
||||
message="Opravdu chcete ukončit všechny ostatní relace? Budete odhlášeni ze všech zařízení kromě tohoto."
|
||||
confirmText="Odhlásit vše"
|
||||
cancelText="Zrušit"
|
||||
type="warning"
|
||||
loading={deleting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user