149 lines
5.9 KiB
React
149 lines
5.9 KiB
React
|
|
import React, { useState, useEffect } from 'react'
|
|||
|
|
import { useAuth } from './auth/AuthContext'
|
|||
|
|
import Toast from './Toast'
|
|||
|
|
import './Tracking.css'
|
|||
|
|
|
|||
|
|
function TrackingAccess({ onNavigate }) {
|
|||
|
|
const { authFetch } = useAuth()
|
|||
|
|
const [generating, setGenerating] = useState(false)
|
|||
|
|
const [copied, setCopied] = useState(false)
|
|||
|
|
const [trackers, setTrackers] = useState([])
|
|||
|
|
const [tracked, setTracked] = useState([])
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [toastMessage, setToastMessage] = useState(null)
|
|||
|
|
|
|||
|
|
// Загрузка списков при монтировании
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchAccessData()
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const fetchAccessData = async () => {
|
|||
|
|
setLoading(true)
|
|||
|
|
try {
|
|||
|
|
const res = await authFetch('/api/tracking/access')
|
|||
|
|
if (res.ok) {
|
|||
|
|
const data = await res.json()
|
|||
|
|
setTrackers(data.trackers || [])
|
|||
|
|
setTracked(data.tracked || [])
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Error fetching access data:', err)
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCreateInvite = async () => {
|
|||
|
|
setGenerating(true)
|
|||
|
|
try {
|
|||
|
|
const res = await authFetch('/api/tracking/invite', { method: 'POST' })
|
|||
|
|
if (res.ok) {
|
|||
|
|
const data = await res.json()
|
|||
|
|
await navigator.clipboard.writeText(data.invite_url)
|
|||
|
|
setCopied(true)
|
|||
|
|
setToastMessage({ text: 'Ссылка скопирована! Действует 1 час', type: 'success' })
|
|||
|
|
setTimeout(() => setCopied(false), 3000)
|
|||
|
|
} else {
|
|||
|
|
setToastMessage({ text: 'Ошибка создания ссылки', type: 'error' })
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
setToastMessage({ text: 'Ошибка создания ссылки', type: 'error' })
|
|||
|
|
} finally {
|
|||
|
|
setGenerating(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleRemoveTracker = async (relationId) => {
|
|||
|
|
if (!window.confirm('Запретить этому пользователю видеть вашу статистику?')) return
|
|||
|
|
try {
|
|||
|
|
const res = await authFetch(`/api/tracking/trackers/${relationId}`, { method: 'DELETE' })
|
|||
|
|
if (res.ok) {
|
|||
|
|
setTrackers(prev => prev.filter(t => t.relation_id !== relationId))
|
|||
|
|
setToastMessage({ text: 'Доступ отозван', type: 'success' })
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
setToastMessage({ text: 'Ошибка', type: 'error' })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleRemoveTracked = async (relationId) => {
|
|||
|
|
if (!window.confirm('Прекратить отслеживать этого пользователя?')) return
|
|||
|
|
try {
|
|||
|
|
const res = await authFetch(`/api/tracking/tracked/${relationId}`, { method: 'DELETE' })
|
|||
|
|
if (res.ok) {
|
|||
|
|
setTracked(prev => prev.filter(t => t.relation_id !== relationId))
|
|||
|
|
setToastMessage({ text: 'Отслеживание прекращено', type: 'success' })
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
setToastMessage({ text: 'Ошибка', type: 'error' })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="tracking-access-screen max-w-2xl mx-auto">
|
|||
|
|
{/* Заголовок с крестиком */}
|
|||
|
|
<div className="tracking-header">
|
|||
|
|
<h2 className="text-2xl font-semibold text-gray-800">Управление доступами</h2>
|
|||
|
|
</div>
|
|||
|
|
<button className="close-x-button" onClick={() => window.history.back()}>✕</button>
|
|||
|
|
|
|||
|
|
{/* Секция создания ссылки */}
|
|||
|
|
<div className="access-section">
|
|||
|
|
<h3>Поделиться статистикой</h3>
|
|||
|
|
<p className="section-hint">Создайте одноразовую ссылку (действует 1 час)</p>
|
|||
|
|
<button
|
|||
|
|
className={`create-invite-btn ${copied ? 'copied' : ''}`}
|
|||
|
|
onClick={handleCreateInvite}
|
|||
|
|
disabled={generating}
|
|||
|
|
>
|
|||
|
|
{generating ? 'Создание...' : copied ? '✓ Ссылка скопирована' : 'Создать и скопировать ссылку'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Список: кто меня отслеживает */}
|
|||
|
|
<div className="access-section">
|
|||
|
|
<h3>Меня отслеживают ({trackers.length})</h3>
|
|||
|
|
{trackers.length === 0 ? (
|
|||
|
|
<p className="empty-list">Пока никто не отслеживает вашу статистику</p>
|
|||
|
|
) : (
|
|||
|
|
trackers.map(t => (
|
|||
|
|
<div key={t.relation_id} className="access-item">
|
|||
|
|
<span className="access-item-name">{t.name}</span>
|
|||
|
|
<button className="remove-btn" onClick={() => handleRemoveTracker(t.relation_id)}>
|
|||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|||
|
|
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Список: кого я отслеживаю */}
|
|||
|
|
<div className="access-section">
|
|||
|
|
<h3>Я отслеживаю ({tracked.length})</h3>
|
|||
|
|
{tracked.length === 0 ? (
|
|||
|
|
<p className="empty-list">Вы пока никого не отслеживаете</p>
|
|||
|
|
) : (
|
|||
|
|
tracked.map(t => (
|
|||
|
|
<div key={t.relation_id} className="access-item">
|
|||
|
|
<span className="access-item-name">{t.name}</span>
|
|||
|
|
<button className="remove-btn" onClick={() => handleRemoveTracked(t.relation_id)}>
|
|||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|||
|
|
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{toastMessage && (
|
|||
|
|
<Toast message={toastMessage.text} type={toastMessage.type} onClose={() => setToastMessage(null)} />
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default TrackingAccess
|