Files
play-life/play-life-web/src/components/TrackingAccess.jsx
poignatov 867e8803bd
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m18s
4.24.3: Обновление данных экрана доступа
2026-02-06 15:59:33 +03:00

159 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { useAuth } from './auth/AuthContext'
import Toast from './Toast'
import './Tracking.css'
function TrackingAccess({ onNavigate, activeTab }) {
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)
const prevActiveTabRef = useRef(null)
const fetchAccessData = useCallback(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)
}
}, [authFetch])
// Загрузка списков при монтировании
useEffect(() => {
fetchAccessData()
}, [fetchAccessData])
// Обновление данных при открытии экрана
useEffect(() => {
// Проверяем, что экран только что открылся (activeTab стал 'tracking-access')
if (activeTab === 'tracking-access' && prevActiveTabRef.current !== 'tracking-access') {
fetchAccessData()
}
prevActiveTabRef.current = activeTab
}, [activeTab, fetchAccessData])
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