4.23.0: Добавлено отслеживание и улучшен UI
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m31s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m31s
This commit is contained in:
148
play-life-web/src/components/TrackingAccess.jsx
Normal file
148
play-life-web/src/components/TrackingAccess.jsx
Normal 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
|
||||
Reference in New Issue
Block a user