4.23.0: Добавлено отслеживание и улучшен UI
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m31s

This commit is contained in:
poignatov
2026-02-05 18:36:14 +03:00
parent d6d40f4f86
commit 6e9e2db23e
12 changed files with 1817 additions and 104 deletions

View File

@@ -0,0 +1,148 @@
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