basic frontend
This commit is contained in:
256
frontend/src/components/CounterDetail.tsx
Normal file
256
frontend/src/components/CounterDetail.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { useState, useEffect } 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, Edit, Trash2, Calendar, TrendingUp } 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);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadCounterData();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const loadCounterData = 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);
|
||||
}
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user