basic frontend #3
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { AuthProvider } from './hooks/useAuth';
|
import { AuthProvider } from './hooks/useAuth';
|
||||||
|
import { CountersProvider } from './contexts/CountersContext';
|
||||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
import { Login } from './components/Login';
|
import { Login } from './components/Login';
|
||||||
@@ -12,6 +13,7 @@ import './App.css';
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<CountersProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -34,6 +36,7 @@ function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
</CountersProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useCounters } from '../hooks/useCounters';
|
|
||||||
import { CounterWithStats } from '../types';
|
import { CounterWithStats } from '../types';
|
||||||
import { Plus, Minus, Edit, Trash2, MoreVertical } from 'lucide-react';
|
import { Plus, Minus, Edit, Trash2, MoreVertical } from 'lucide-react';
|
||||||
|
|
||||||
interface CounterCardProps {
|
interface CounterCardProps {
|
||||||
counter: CounterWithStats;
|
counter: CounterWithStats;
|
||||||
|
onIncrement: (id: number | string, value: number) => Promise<void>;
|
||||||
|
onDelete: (id: number | string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CounterCard: React.FC<CounterCardProps> = ({ counter }) => {
|
export const CounterCard: React.FC<CounterCardProps> = ({ counter, onIncrement, onDelete }) => {
|
||||||
const { incrementCounter, deleteCounter } = useCounters();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
|
||||||
const handleIncrement = async (value: number) => {
|
const handleIncrement = async (value: number) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await incrementCounter(counter.id, value);
|
await onIncrement(counter.id, value);
|
||||||
|
console.log('Counter incremented:', counter.id, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to increment counter:', error);
|
console.error('Failed to increment counter:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -27,7 +28,7 @@ export const CounterCard: React.FC<CounterCardProps> = ({ counter }) => {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (window.confirm('Are you sure you want to delete this counter?')) {
|
if (window.confirm('Are you sure you want to delete this counter?')) {
|
||||||
try {
|
try {
|
||||||
await deleteCounter(counter.id);
|
await onDelete(counter.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete counter:', error);
|
console.error('Failed to delete counter:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useCounters } from '../hooks/useCounters';
|
import { useCountersContext } from '../contexts/CountersContext';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import { CounterWithStats, CounterEntry } from '../types';
|
import { CounterEntry } from '../types';
|
||||||
import { countersAPI } from '../services/api';
|
|
||||||
import { ArrowLeft, Plus, Minus, Trash2, Calendar } from 'lucide-react';
|
import { ArrowLeft, Plus, Minus, Trash2, Calendar } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
@@ -11,91 +10,35 @@ export const CounterDetail: React.FC = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const { incrementCounter, deleteCounter } = useCounters();
|
const { counters, incrementCounter, deleteCounter, getCounterEntries } = useCountersContext();
|
||||||
|
|
||||||
const [counter, setCounter] = useState<CounterWithStats | null>(null);
|
// Get the current counter from the useCounters hook
|
||||||
|
const counter = counters.find(c => c.id.toString() === id) || null;
|
||||||
const [entries, setEntries] = useState<CounterEntry[]>([]);
|
const [entries, setEntries] = useState<CounterEntry[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isIncrementing, setIsIncrementing] = useState(false);
|
const [isIncrementing, setIsIncrementing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadCounterData = useCallback(async () => {
|
// Load entries when component mounts or counter changes
|
||||||
if (!id) return;
|
useEffect(() => {
|
||||||
|
const loadEntries = async () => {
|
||||||
|
if (!id || !counter) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isAuthenticated) {
|
const counterEntries = await getCounterEntries(id);
|
||||||
// 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);
|
setEntries(counterEntries);
|
||||||
} else {
|
|
||||||
setError('Counter not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.error || 'Failed to load counter');
|
setError(err.message || 'Failed to load counter entries');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [id, isAuthenticated]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
loadEntries();
|
||||||
if (id) {
|
}, [id, counter, getCounterEntries]);
|
||||||
loadCounterData();
|
|
||||||
}
|
|
||||||
}, [id, loadCounterData]);
|
|
||||||
|
|
||||||
const handleIncrement = async (value: number) => {
|
const handleIncrement = async (value: number) => {
|
||||||
if (!counter) return;
|
if (!counter) return;
|
||||||
@@ -103,8 +46,8 @@ export const CounterDetail: React.FC = () => {
|
|||||||
setIsIncrementing(true);
|
setIsIncrementing(true);
|
||||||
try {
|
try {
|
||||||
await incrementCounter(counter.id, value);
|
await incrementCounter(counter.id, value);
|
||||||
// Reload data to get updated values
|
console.log('Counter incremented:', counter.id, value);
|
||||||
await loadCounterData();
|
// Entries will be reloaded automatically when counter changes
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to increment counter:', error);
|
console.error('Failed to increment counter:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -150,68 +93,100 @@ export const CounterDetail: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600"
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-6 w-6" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex-1">
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{counter.name}</h1>
|
<h1 className="text-2xl font-bold text-gray-900">{counter.name}</h1>
|
||||||
{counter.description && (
|
{counter.description && (
|
||||||
<p className="text-gray-600 mt-1">{counter.description}</p>
|
<p className="text-gray-600 mt-1">{counter.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="btn btn-danger flex items-center space-x-2"
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-5 w-5" />
|
||||||
<span>Delete</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="card p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-gray-600">Total Value</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{counter.total_value}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Counter Value and Controls */}
|
<div className="card p-4">
|
||||||
<div className="card p-8 text-center">
|
<div className="text-center">
|
||||||
<div className="text-6xl font-bold text-primary-600 mb-4">
|
<p className="text-sm font-medium text-gray-600">Today</p>
|
||||||
{counter.total_value}
|
<p className="text-2xl font-bold text-gray-900">{counter.today_value}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg text-gray-600 mb-8">Total Count</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center space-x-4">
|
<div className="card p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-gray-600">This Week</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{counter.week_value}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-gray-600">This Month</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{counter.month_value}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Increment Controls */}
|
||||||
|
<div className="card p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||||
|
<div className="flex space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleIncrement(-1)}
|
onClick={() => handleIncrement(-1)}
|
||||||
disabled={isIncrementing}
|
disabled={isIncrementing}
|
||||||
className="btn btn-secondary text-2xl px-8 py-4 disabled:opacity-50"
|
className="btn btn-secondary flex items-center space-x-2 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Minus className="h-8 w-8" />
|
<Minus className="h-5 w-5" />
|
||||||
|
<span>-1</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleIncrement(1)}
|
onClick={() => handleIncrement(1)}
|
||||||
disabled={isIncrementing}
|
disabled={isIncrementing}
|
||||||
className="btn btn-primary text-2xl px-8 py-4 disabled:opacity-50"
|
className="btn btn-primary flex items-center space-x-2 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Plus className="h-8 w-8" />
|
<Plus className="h-5 w-5" />
|
||||||
|
<span>+1</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Grid */}
|
<button
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
onClick={() => handleIncrement(5)}
|
||||||
<div className="card p-6 text-center">
|
disabled={isIncrementing}
|
||||||
<div className="text-2xl font-bold text-green-600 mb-2">{counter.today_value}</div>
|
className="btn btn-primary flex items-center space-x-2 disabled:opacity-50"
|
||||||
<div className="text-sm text-gray-600">Today</div>
|
>
|
||||||
</div>
|
<Plus className="h-5 w-5" />
|
||||||
<div className="card p-6 text-center">
|
<span>+5</span>
|
||||||
<div className="text-2xl font-bold text-blue-600 mb-2">{counter.week_value}</div>
|
</button>
|
||||||
<div className="text-sm text-gray-600">This Week</div>
|
|
||||||
</div>
|
<button
|
||||||
<div className="card p-6 text-center">
|
onClick={() => handleIncrement(10)}
|
||||||
<div className="text-2xl font-bold text-purple-600 mb-2">{counter.month_value}</div>
|
disabled={isIncrementing}
|
||||||
<div className="text-sm text-gray-600">This Month</div>
|
className="btn btn-primary flex items-center space-x-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
<span>+10</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -234,17 +209,14 @@ export const CounterDetail: React.FC = () => {
|
|||||||
className="flex justify-between items-center py-2 px-4 bg-gray-50 rounded-lg"
|
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="flex items-center space-x-3">
|
||||||
<div className={`w-3 h-3 rounded-full ${
|
<div className="text-sm text-gray-600">
|
||||||
entry.value > 0 ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`} />
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{format(new Date(entry.date), 'MMM dd, yyyy')}
|
{format(new Date(entry.date), 'MMM dd, yyyy')}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-sm font-medium ${
|
</div>
|
||||||
entry.value > 0 ? 'text-green-600' : 'text-red-600'
|
<div className="flex items-center space-x-2">
|
||||||
}`}>
|
<span className={`font-medium ${entry.value >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
{entry.value > 0 ? '+' : ''}{entry.value}
|
{entry.value > 0 ? '+' : ''}{entry.value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useCounters } from '../hooks/useCounters';
|
import { useCountersContext } from '../contexts/CountersContext';
|
||||||
import { CreateCounterRequest } from '../types';
|
import { CreateCounterRequest } from '../types';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ interface CreateCounterModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CreateCounterModal: React.FC<CreateCounterModalProps> = ({ onClose }) => {
|
export const CreateCounterModal: React.FC<CreateCounterModalProps> = ({ onClose }) => {
|
||||||
const { createCounter } = useCounters();
|
const { createCounter } = useCountersContext();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useCounters } from '../hooks/useCounters';
|
import { useCountersContext } from '../contexts/CountersContext';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import { CreateCounterModal } from './CreateCounterModal';
|
import { CreateCounterModal } from './CreateCounterModal';
|
||||||
import { CounterCard } from './CounterCard';
|
import { CounterCard } from './CounterCard';
|
||||||
import { Plus, Search, TrendingUp, Calendar, Clock } from 'lucide-react';
|
import { Plus, Search, TrendingUp, Calendar, Clock } from 'lucide-react';
|
||||||
|
|
||||||
export const Dashboard: React.FC = () => {
|
export const Dashboard: React.FC = () => {
|
||||||
const { counters, isLoading, error, searchCounters } = useCounters();
|
console.log('Dashboard: Component rendering...');
|
||||||
|
const { counters, isLoading, error, searchCounters, version, incrementCounter, deleteCounter } = useCountersContext();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -16,6 +17,11 @@ export const Dashboard: React.FC = () => {
|
|||||||
searchCounters(query);
|
searchCounters(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Dashboard: Counters changed:', counters, 'version:', version);
|
||||||
|
}, [counters, version]);
|
||||||
|
|
||||||
|
console.log('Dashboard: Current counters:', counters, 'version:', version);
|
||||||
const totalCounters = counters?.length || 0;
|
const totalCounters = counters?.length || 0;
|
||||||
const totalValue = counters?.reduce((sum, counter) => sum + counter.total_value, 0) || 0;
|
const totalValue = counters?.reduce((sum, counter) => sum + counter.total_value, 0) || 0;
|
||||||
const todayValue = counters?.reduce((sum, counter) => sum + counter.today_value, 0) || 0;
|
const todayValue = counters?.reduce((sum, counter) => sum + counter.today_value, 0) || 0;
|
||||||
@@ -41,6 +47,7 @@ export const Dashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
className="btn btn-primary flex items-center space-x-2"
|
className="btn btn-primary flex items-center space-x-2"
|
||||||
@@ -128,7 +135,12 @@ export const Dashboard: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{counters?.map((counter) => (
|
{counters?.map((counter) => (
|
||||||
<CounterCard key={counter.id} counter={counter} />
|
<CounterCard
|
||||||
|
key={counter.id}
|
||||||
|
counter={counter}
|
||||||
|
onIncrement={incrementCounter}
|
||||||
|
onDelete={deleteCounter}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
41
frontend/src/contexts/CountersContext.tsx
Normal file
41
frontend/src/contexts/CountersContext.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { createContext, useContext, ReactNode } from 'react';
|
||||||
|
import { CounterWithStats, CreateCounterRequest, UpdateCounterRequest, CounterEntry } from '../types';
|
||||||
|
import { useCounters } from '../hooks/useCounters';
|
||||||
|
|
||||||
|
interface CountersContextType {
|
||||||
|
counters: CounterWithStats[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
version: number;
|
||||||
|
createCounter: (data: CreateCounterRequest) => Promise<any>;
|
||||||
|
updateCounter: (id: number | string, data: UpdateCounterRequest) => Promise<any>;
|
||||||
|
deleteCounter: (id: number | string) => Promise<void>;
|
||||||
|
incrementCounter: (id: number | string, value: number) => Promise<any>;
|
||||||
|
getCounterEntries: (id: number | string) => Promise<CounterEntry[]>;
|
||||||
|
searchCounters: (search: string) => void;
|
||||||
|
refreshCounters: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountersContext = createContext<CountersContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface CountersProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CountersProvider: React.FC<CountersProviderProps> = ({ children }) => {
|
||||||
|
const countersData = useCounters();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CountersContext.Provider value={countersData}>
|
||||||
|
{children}
|
||||||
|
</CountersContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCountersContext = (): CountersContextType => {
|
||||||
|
const context = useContext(CountersContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useCountersContext must be used within a CountersProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { CounterWithStats, AnonymousCounter, CreateCounterRequest, UpdateCounterRequest, IncrementRequest } from '../types';
|
import { CounterWithStats, AnonymousCounter, CreateCounterRequest, UpdateCounterRequest, CounterEntry } from '../types';
|
||||||
import { countersAPI } from '../services/api';
|
import { countersAPI } from '../services/api';
|
||||||
import { localStorageService } from '../services/localStorage';
|
import { localStorageService } from '../services/localStorage';
|
||||||
import { useAuth } from './useAuth';
|
import { useAuth } from './useAuth';
|
||||||
@@ -7,25 +7,37 @@ import { useAuth } from './useAuth';
|
|||||||
export const useCounters = () => {
|
export const useCounters = () => {
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const [counters, setCounters] = useState<CounterWithStats[]>([]);
|
const [counters, setCounters] = useState<CounterWithStats[]>([]);
|
||||||
const [anonymousCounters, setAnonymousCounters] = useState<AnonymousCounter[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [version, setVersion] = useState(0); // Force re-renders
|
||||||
|
|
||||||
// Load counters based on authentication status
|
// Load counters based on authentication status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
|
||||||
loadCounters();
|
loadCounters();
|
||||||
} else {
|
|
||||||
loadAnonymousCounters();
|
|
||||||
}
|
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Counters:', counters);
|
||||||
|
}, [counters]);
|
||||||
|
|
||||||
const loadCounters = async (search?: string) => {
|
const loadCounters = async (search?: string) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
// Load from API
|
||||||
const response = await countersAPI.getCounters(search);
|
const response = await countersAPI.getCounters(search);
|
||||||
setCounters(response.data);
|
console.log('Setting authenticated counters:', response.data);
|
||||||
|
setCounters([...response.data]); // Ensure new array reference
|
||||||
|
setVersion(prev => prev + 1); // Force re-render
|
||||||
|
} else {
|
||||||
|
// Load from localStorage and convert to CounterWithStats format
|
||||||
|
const anonymousCounters = localStorageService.getAnonymousCounters();
|
||||||
|
const convertedCounters = convertAnonymousToDisplay(anonymousCounters);
|
||||||
|
console.log('Setting anonymous counters:', convertedCounters);
|
||||||
|
setCounters([...convertedCounters]); // Ensure new array reference
|
||||||
|
setVersion(prev => prev + 1); // Force re-render
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.error || 'Failed to load counters');
|
setError(err.response?.data?.error || 'Failed to load counters');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -33,9 +45,48 @@ export const useCounters = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAnonymousCounters = () => {
|
const convertAnonymousToDisplay = (anonymousCounters: AnonymousCounter[]): CounterWithStats[] => {
|
||||||
const stored = localStorageService.getAnonymousCounters();
|
console.log('Converting anonymous counters:', anonymousCounters);
|
||||||
setAnonymousCounters(stored);
|
const converted = anonymousCounters.map(counter => {
|
||||||
|
const today = localStorageService.getTodayString();
|
||||||
|
const todayValue = counter.entries[today] || 0;
|
||||||
|
|
||||||
|
const weekValue = Object.keys(counter.entries)
|
||||||
|
.filter((date) => {
|
||||||
|
const entryDate = new Date(date);
|
||||||
|
const weekAgo = new Date();
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
|
return entryDate >= weekAgo;
|
||||||
|
})
|
||||||
|
.reduce((sum, date) => sum + (counter.entries[date] as number), 0);
|
||||||
|
|
||||||
|
const monthValue = Object.keys(counter.entries)
|
||||||
|
.filter((date) => {
|
||||||
|
const entryDate = new Date(date);
|
||||||
|
const monthAgo = new Date();
|
||||||
|
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||||
|
return entryDate >= monthAgo;
|
||||||
|
})
|
||||||
|
.reduce((sum, date) => sum + (counter.entries[date] as number), 0);
|
||||||
|
|
||||||
|
const entryCount = Object.keys(counter.entries).reduce((sum: number, date: string) => sum + Math.abs(counter.entries[date] as number), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: counter.id,
|
||||||
|
name: counter.name,
|
||||||
|
description: counter.description,
|
||||||
|
created_at: counter.created_at,
|
||||||
|
updated_at: counter.updated_at,
|
||||||
|
total_value: counter.total_value,
|
||||||
|
today_value: todayValue,
|
||||||
|
week_value: weekValue,
|
||||||
|
month_value: monthValue,
|
||||||
|
entry_count: entryCount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Converted counters:', converted);
|
||||||
|
return converted;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createCounter = async (data: CreateCounterRequest) => {
|
const createCounter = async (data: CreateCounterRequest) => {
|
||||||
@@ -60,7 +111,7 @@ export const useCounters = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
localStorageService.addAnonymousCounter(newCounter);
|
localStorageService.addAnonymousCounter(newCounter);
|
||||||
loadAnonymousCounters();
|
await loadCounters(); // Refresh the list
|
||||||
return newCounter;
|
return newCounter;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -78,7 +129,7 @@ export const useCounters = () => {
|
|||||||
// Update anonymous counter
|
// Update anonymous counter
|
||||||
const updated = { ...data, updated_at: new Date().toISOString() };
|
const updated = { ...data, updated_at: new Date().toISOString() };
|
||||||
localStorageService.updateAnonymousCounter(id as string, updated);
|
localStorageService.updateAnonymousCounter(id as string, updated);
|
||||||
loadAnonymousCounters();
|
await loadCounters(); // Refresh the list
|
||||||
return { id, ...updated };
|
return { id, ...updated };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -94,7 +145,7 @@ export const useCounters = () => {
|
|||||||
} else {
|
} else {
|
||||||
// Delete anonymous counter
|
// Delete anonymous counter
|
||||||
localStorageService.deleteAnonymousCounter(id as string);
|
localStorageService.deleteAnonymousCounter(id as string);
|
||||||
loadAnonymousCounters();
|
await loadCounters(); // Refresh the list
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,8 +161,13 @@ export const useCounters = () => {
|
|||||||
} else {
|
} else {
|
||||||
// Increment anonymous counter
|
// Increment anonymous counter
|
||||||
const today = localStorageService.getTodayString();
|
const today = localStorageService.getTodayString();
|
||||||
const counter = anonymousCounters.find((c: AnonymousCounter) => c.id === id);
|
const counters = localStorageService.getAnonymousCounters();
|
||||||
if (counter) {
|
const counter = counters.find((c: AnonymousCounter) => c.id === id);
|
||||||
|
|
||||||
|
if (!counter) {
|
||||||
|
throw new Error('Counter not found');
|
||||||
|
}
|
||||||
|
|
||||||
const newTotal = counter.total_value + value;
|
const newTotal = counter.total_value + value;
|
||||||
const newEntries = { ...counter.entries };
|
const newEntries = { ...counter.entries };
|
||||||
newEntries[today] = (newEntries[today] || 0) + value;
|
newEntries[today] = (newEntries[today] || 0) + value;
|
||||||
@@ -123,69 +179,54 @@ export const useCounters = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
localStorageService.updateAnonymousCounter(id as string, updated);
|
localStorageService.updateAnonymousCounter(id as string, updated);
|
||||||
loadAnonymousCounters();
|
console.log('Increment completed, reloading counters...');
|
||||||
|
await loadCounters(); // Refresh the list
|
||||||
return { id, ...updated };
|
return { id, ...updated };
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCounterEntries = async (id: number | string): Promise<CounterEntry[]> => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
try {
|
||||||
|
const response = await countersAPI.getCounterEntries(id as number);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(err.response?.data?.error || 'Failed to load counter entries');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get entries from localStorage
|
||||||
|
const counters = localStorageService.getAnonymousCounters();
|
||||||
|
const counter = counters.find((c: AnonymousCounter) => c.id === id);
|
||||||
|
|
||||||
|
if (!counter) {
|
||||||
throw new Error('Counter not found');
|
throw new Error('Counter not found');
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const searchCounters = (search: string) => {
|
return Object.entries(counter.entries || {}).map(([date, value], index) => ({
|
||||||
if (isAuthenticated) {
|
id: index + 1,
|
||||||
loadCounters(search);
|
counter_id: parseInt(counter.id as string),
|
||||||
} else {
|
value: value as number,
|
||||||
// Filter anonymous counters locally
|
date: date,
|
||||||
const filtered = localStorageService.getAnonymousCounters().filter(counter =>
|
created_at: counter.updated_at,
|
||||||
counter.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
counter.description.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
setAnonymousCounters(filtered);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert anonymous counters to CounterWithStats format for consistent UI
|
|
||||||
const getDisplayCounters = (): CounterWithStats[] => {
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return counters || [];
|
|
||||||
} else {
|
|
||||||
return (anonymousCounters || []).map((counter: AnonymousCounter) => ({
|
|
||||||
id: parseInt(counter.id), // Convert string ID to number for display
|
|
||||||
name: counter.name,
|
|
||||||
description: counter.description,
|
|
||||||
created_at: counter.created_at,
|
|
||||||
updated_at: counter.updated_at,
|
|
||||||
total_value: counter.total_value,
|
|
||||||
today_value: counter.entries[localStorageService.getTodayString()] || 0,
|
|
||||||
week_value: Object.keys(counter.entries)
|
|
||||||
.filter((date) => {
|
|
||||||
const entryDate = new Date(date);
|
|
||||||
const weekAgo = new Date();
|
|
||||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
||||||
return entryDate >= weekAgo;
|
|
||||||
})
|
|
||||||
.reduce((sum, date) => sum + (counter.entries[date] as number), 0),
|
|
||||||
month_value: Object.keys(counter.entries)
|
|
||||||
.filter((date) => {
|
|
||||||
const entryDate = new Date(date);
|
|
||||||
const monthAgo = new Date();
|
|
||||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
||||||
return entryDate >= monthAgo;
|
|
||||||
})
|
|
||||||
.reduce((sum, date) => sum + (counter.entries[date] as number), 0),
|
|
||||||
entry_count: Object.keys(counter.entries).reduce((sum: number, date: string) => sum + Math.abs(counter.entries[date] as number), 0),
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchCounters = (search: string) => {
|
||||||
|
loadCounters(search);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
counters: getDisplayCounters(),
|
counters,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
version, // Add version to force re-renders
|
||||||
createCounter,
|
createCounter,
|
||||||
updateCounter,
|
updateCounter,
|
||||||
deleteCounter,
|
deleteCounter,
|
||||||
incrementCounter,
|
incrementCounter,
|
||||||
|
getCounterEntries,
|
||||||
searchCounters,
|
searchCounters,
|
||||||
refreshCounters: isAuthenticated ? loadCounters : loadAnonymousCounters,
|
refreshCounters: loadCounters,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -23,7 +23,12 @@ export interface CounterEntry {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CounterWithStats extends Counter {
|
export interface CounterWithStats {
|
||||||
|
id: number | string; // Allow both number (authenticated) and string (anonymous) IDs
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
total_value: number;
|
total_value: number;
|
||||||
today_value: number;
|
today_value: number;
|
||||||
week_value: number;
|
week_value: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user