Files
counter/frontend/src/components/CounterDetail.tsx
aovantsev 5cde4bf00a
All checks were successful
continuous-integration/drone/push Build is passing
fix
2025-10-03 14:50:18 +03:00

257 lines
9.0 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useCounters } from '../hooks/useCounters';
import { useAuth } from '../hooks/useAuth';
import { CounterWithStats, CounterEntry } from '../types';
import { countersAPI } from '../services/api';
import { ArrowLeft, Plus, Minus, Trash2, Calendar } from 'lucide-react';
import { format } from 'date-fns';
export const CounterDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { isAuthenticated } = useAuth();
const { incrementCounter, deleteCounter } = useCounters();
const [counter, setCounter] = useState<CounterWithStats | null>(null);
const [entries, setEntries] = useState<CounterEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isIncrementing, setIsIncrementing] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadCounterData = useCallback(async () => {
if (!id) return;
setIsLoading(true);
setError(null);
try {
if (isAuthenticated) {
// Load from API
const [counterResponse, entriesResponse] = await Promise.all([
countersAPI.getCounter(parseInt(id)),
countersAPI.getCounterEntries(parseInt(id))
]);
setCounter(counterResponse.data);
setEntries(entriesResponse.data);
} else {
// Load from localStorage
const counters = JSON.parse(localStorage.getItem('anonymous_counters') || '[]');
const foundCounter = counters.find((c: any) => c.id === id);
if (foundCounter) {
// Convert to CounterWithStats format
const today = new Date().toISOString().split('T')[0];
const counterWithStats: CounterWithStats = {
id: parseInt(foundCounter.id),
name: foundCounter.name,
description: foundCounter.description,
created_at: foundCounter.created_at,
updated_at: foundCounter.updated_at,
total_value: foundCounter.total_value,
today_value: foundCounter.entries[today] || 0,
week_value: Object.entries(foundCounter.entries)
.filter(([date]) => {
const entryDate = new Date(date);
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return entryDate >= weekAgo;
})
.reduce((sum, [, value]) => sum + (value as number), 0),
month_value: Object.entries(foundCounter.entries)
.filter(([date]) => {
const entryDate = new Date(date);
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
return entryDate >= monthAgo;
})
.reduce((sum, [, value]) => sum + (value as number), 0),
entry_count: Object.values(foundCounter.entries).reduce((sum: number, value: unknown) => sum + Math.abs(value as number), 0),
};
setCounter(counterWithStats);
// Convert entries to CounterEntry format
const counterEntries: CounterEntry[] = Object.entries(foundCounter.entries)
.map(([date, value]) => ({
id: Math.random(), // Generate random ID for display
counter_id: parseInt(foundCounter.id),
value: value as number,
date: date,
created_at: new Date().toISOString(),
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
setEntries(counterEntries);
} else {
setError('Counter not found');
}
}
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to load counter');
} finally {
setIsLoading(false);
}
}, [id, isAuthenticated]);
useEffect(() => {
if (id) {
loadCounterData();
}
}, [id, loadCounterData]);
const handleIncrement = async (value: number) => {
if (!counter) return;
setIsIncrementing(true);
try {
await incrementCounter(counter.id, value);
// Reload data to get updated values
await loadCounterData();
} catch (error) {
console.error('Failed to increment counter:', error);
} finally {
setIsIncrementing(false);
}
};
const handleDelete = async () => {
if (!counter) return;
if (window.confirm('Are you sure you want to delete this counter?')) {
try {
await deleteCounter(counter.id);
navigate('/');
} catch (error) {
console.error('Failed to delete counter:', error);
}
}
};
if (isLoading) {
return (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
);
}
if (error || !counter) {
return (
<div className="text-center py-12">
<div className="text-red-600 mb-4">{error || 'Counter not found'}</div>
<button
onClick={() => navigate('/')}
className="btn btn-primary"
>
Back to Dashboard
</button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center space-x-4">
<button
onClick={() => navigate('/')}
className="p-2 text-gray-400 hover:text-gray-600"
>
<ArrowLeft className="h-6 w-6" />
</button>
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900">{counter.name}</h1>
{counter.description && (
<p className="text-gray-600 mt-1">{counter.description}</p>
)}
</div>
<div className="flex space-x-2">
<button
onClick={handleDelete}
className="btn btn-danger flex items-center space-x-2"
>
<Trash2 className="h-4 w-4" />
<span>Delete</span>
</button>
</div>
</div>
{/* Counter Value and Controls */}
<div className="card p-8 text-center">
<div className="text-6xl font-bold text-primary-600 mb-4">
{counter.total_value}
</div>
<div className="text-lg text-gray-600 mb-8">Total Count</div>
<div className="flex justify-center space-x-4">
<button
onClick={() => handleIncrement(-1)}
disabled={isIncrementing}
className="btn btn-secondary text-2xl px-8 py-4 disabled:opacity-50"
>
<Minus className="h-8 w-8" />
</button>
<button
onClick={() => handleIncrement(1)}
disabled={isIncrementing}
className="btn btn-primary text-2xl px-8 py-4 disabled:opacity-50"
>
<Plus className="h-8 w-8" />
</button>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="card p-6 text-center">
<div className="text-2xl font-bold text-green-600 mb-2">{counter.today_value}</div>
<div className="text-sm text-gray-600">Today</div>
</div>
<div className="card p-6 text-center">
<div className="text-2xl font-bold text-blue-600 mb-2">{counter.week_value}</div>
<div className="text-sm text-gray-600">This Week</div>
</div>
<div className="card p-6 text-center">
<div className="text-2xl font-bold text-purple-600 mb-2">{counter.month_value}</div>
<div className="text-sm text-gray-600">This Month</div>
</div>
</div>
{/* Recent Entries */}
<div className="card p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<Calendar className="h-5 w-5 mr-2" />
Recent Entries
</h2>
{(entries?.length || 0) === 0 ? (
<div className="text-center py-8 text-gray-500">
No entries yet. Start by incrementing your counter!
</div>
) : (
<div className="space-y-3">
{entries?.slice(0, 10).map((entry) => (
<div
key={entry.id}
className="flex justify-between items-center py-2 px-4 bg-gray-50 rounded-lg"
>
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${
entry.value > 0 ? 'bg-green-500' : 'bg-red-500'
}`} />
<span className="text-sm text-gray-600">
{format(new Date(entry.date), 'MMM dd, yyyy')}
</span>
</div>
<div className={`text-sm font-medium ${
entry.value > 0 ? 'text-green-600' : 'text-red-600'
}`}>
{entry.value > 0 ? '+' : ''}{entry.value}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};