final fix
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
aovantsev
2025-10-03 16:15:08 +03:00
parent 5cde4bf00a
commit 26acbbc810
8 changed files with 323 additions and 248 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './hooks/useAuth';
import { CountersProvider } from './contexts/CountersContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Layout } from './components/Layout';
import { Login } from './components/Login';
@@ -12,6 +13,7 @@ import './App.css';
function App() {
return (
<AuthProvider>
<CountersProvider>
<Router>
<div className="App">
<Routes>
@@ -34,6 +36,7 @@ function App() {
</Routes>
</div>
</Router>
</CountersProvider>
</AuthProvider>
);
}

View File

@@ -1,22 +1,23 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { useCounters } from '../hooks/useCounters';
import { CounterWithStats } from '../types';
import { Plus, Minus, Edit, Trash2, MoreVertical } from 'lucide-react';
interface CounterCardProps {
counter: CounterWithStats;
onIncrement: (id: number | string, value: number) => Promise<void>;
onDelete: (id: number | string) => Promise<void>;
}
export const CounterCard: React.FC<CounterCardProps> = ({ counter }) => {
const { incrementCounter, deleteCounter } = useCounters();
export const CounterCard: React.FC<CounterCardProps> = ({ counter, onIncrement, onDelete }) => {
const [isLoading, setIsLoading] = useState(false);
const [showMenu, setShowMenu] = useState(false);
const handleIncrement = async (value: number) => {
setIsLoading(true);
try {
await incrementCounter(counter.id, value);
await onIncrement(counter.id, value);
console.log('Counter incremented:', counter.id, value);
} catch (error) {
console.error('Failed to increment counter:', error);
} finally {
@@ -27,7 +28,7 @@ export const CounterCard: React.FC<CounterCardProps> = ({ counter }) => {
const handleDelete = async () => {
if (window.confirm('Are you sure you want to delete this counter?')) {
try {
await deleteCounter(counter.id);
await onDelete(counter.id);
} catch (error) {
console.error('Failed to delete counter:', error);
}

View File

@@ -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 { useCounters } from '../hooks/useCounters';
import { useCountersContext } from '../contexts/CountersContext';
import { useAuth } from '../hooks/useAuth';
import { CounterWithStats, CounterEntry } from '../types';
import { countersAPI } from '../services/api';
import { CounterEntry } from '../types';
import { ArrowLeft, Plus, Minus, Trash2, Calendar } from 'lucide-react';
import { format } from 'date-fns';
@@ -11,91 +10,35 @@ export const CounterDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
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 [isLoading, setIsLoading] = useState(true);
const [isIncrementing, setIsIncrementing] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadCounterData = useCallback(async () => {
if (!id) return;
// Load entries when component mounts or counter changes
useEffect(() => {
const loadEntries = async () => {
if (!id || !counter) 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());
const counterEntries = await getCounterEntries(id);
setEntries(counterEntries);
} else {
setError('Counter not found');
}
}
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to load counter');
setError(err.message || 'Failed to load counter entries');
} finally {
setIsLoading(false);
}
}, [id, isAuthenticated]);
};
useEffect(() => {
if (id) {
loadCounterData();
}
}, [id, loadCounterData]);
loadEntries();
}, [id, counter, getCounterEntries]);
const handleIncrement = async (value: number) => {
if (!counter) return;
@@ -103,8 +46,8 @@ export const CounterDetail: React.FC = () => {
setIsIncrementing(true);
try {
await incrementCounter(counter.id, value);
// Reload data to get updated values
await loadCounterData();
console.log('Counter incremented:', counter.id, value);
// Entries will be reloaded automatically when counter changes
} catch (error) {
console.error('Failed to increment counter:', error);
} finally {
@@ -150,68 +93,100 @@ export const CounterDetail: React.FC = () => {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
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>
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900">{counter.name}</h1>
<div>
<h1 className="text-2xl 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">
</div>
<button
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" />
<span>Delete</span>
<Trash2 className="h-5 w-5" />
</button>
</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>
{/* 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 className="card p-4">
<div className="text-center">
<p className="text-sm font-medium text-gray-600">Today</p>
<p className="text-2xl font-bold text-gray-900">{counter.today_value}</p>
</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
onClick={() => handleIncrement(-1)}
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
onClick={() => handleIncrement(1)}
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>
</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>
<button
onClick={() => handleIncrement(5)}
disabled={isIncrementing}
className="btn btn-primary flex items-center space-x-2 disabled:opacity-50"
>
<Plus className="h-5 w-5" />
<span>+5</span>
</button>
<button
onClick={() => handleIncrement(10)}
disabled={isIncrementing}
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>
@@ -234,17 +209,14 @@ export const CounterDetail: React.FC = () => {
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">
<div 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'
}`}>
</div>
<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}
</span>
</div>
</div>
))}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useCounters } from '../hooks/useCounters';
import { useCountersContext } from '../contexts/CountersContext';
import { CreateCounterRequest } from '../types';
import { X } from 'lucide-react';
@@ -9,7 +9,7 @@ interface CreateCounterModalProps {
}
export const CreateCounterModal: React.FC<CreateCounterModalProps> = ({ onClose }) => {
const { createCounter } = useCounters();
const { createCounter } = useCountersContext();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

View File

@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { useCounters } from '../hooks/useCounters';
import React, { useState, useEffect } from 'react';
import { useCountersContext } from '../contexts/CountersContext';
import { useAuth } from '../hooks/useAuth';
import { CreateCounterModal } from './CreateCounterModal';
import { CounterCard } from './CounterCard';
import { Plus, Search, TrendingUp, Calendar, Clock } from 'lucide-react';
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 [showCreateModal, setShowCreateModal] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
@@ -16,6 +17,11 @@ export const Dashboard: React.FC = () => {
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 totalValue = counters?.reduce((sum, counter) => sum + counter.total_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>
</div>
<button
onClick={() => setShowCreateModal(true)}
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">
{counters?.map((counter) => (
<CounterCard key={counter.id} counter={counter} />
<CounterCard
key={counter.id}
counter={counter}
onIncrement={incrementCounter}
onDelete={deleteCounter}
/>
))}
</div>
)}

View 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;
};

View File

@@ -1,5 +1,5 @@
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 { localStorageService } from '../services/localStorage';
import { useAuth } from './useAuth';
@@ -7,25 +7,37 @@ import { useAuth } from './useAuth';
export const useCounters = () => {
const { isAuthenticated } = useAuth();
const [counters, setCounters] = useState<CounterWithStats[]>([]);
const [anonymousCounters, setAnonymousCounters] = useState<AnonymousCounter[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [version, setVersion] = useState(0); // Force re-renders
// Load counters based on authentication status
useEffect(() => {
if (isAuthenticated) {
loadCounters();
} else {
loadAnonymousCounters();
}
}, [isAuthenticated]);
useEffect(() => {
console.log('Counters:', counters);
}, [counters]);
const loadCounters = async (search?: string) => {
setIsLoading(true);
setError(null);
try {
if (isAuthenticated) {
// Load from API
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) {
setError(err.response?.data?.error || 'Failed to load counters');
} finally {
@@ -33,9 +45,48 @@ export const useCounters = () => {
}
};
const loadAnonymousCounters = () => {
const stored = localStorageService.getAnonymousCounters();
setAnonymousCounters(stored);
const convertAnonymousToDisplay = (anonymousCounters: AnonymousCounter[]): CounterWithStats[] => {
console.log('Converting anonymous counters:', anonymousCounters);
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) => {
@@ -60,7 +111,7 @@ export const useCounters = () => {
};
localStorageService.addAnonymousCounter(newCounter);
loadAnonymousCounters();
await loadCounters(); // Refresh the list
return newCounter;
}
};
@@ -78,7 +129,7 @@ export const useCounters = () => {
// Update anonymous counter
const updated = { ...data, updated_at: new Date().toISOString() };
localStorageService.updateAnonymousCounter(id as string, updated);
loadAnonymousCounters();
await loadCounters(); // Refresh the list
return { id, ...updated };
}
};
@@ -94,7 +145,7 @@ export const useCounters = () => {
} else {
// Delete anonymous counter
localStorageService.deleteAnonymousCounter(id as string);
loadAnonymousCounters();
await loadCounters(); // Refresh the list
}
};
@@ -110,8 +161,13 @@ export const useCounters = () => {
} else {
// Increment anonymous counter
const today = localStorageService.getTodayString();
const counter = anonymousCounters.find((c: AnonymousCounter) => c.id === id);
if (counter) {
const counters = localStorageService.getAnonymousCounters();
const counter = counters.find((c: AnonymousCounter) => c.id === id);
if (!counter) {
throw new Error('Counter not found');
}
const newTotal = counter.total_value + value;
const newEntries = { ...counter.entries };
newEntries[today] = (newEntries[today] || 0) + value;
@@ -123,69 +179,54 @@ export const useCounters = () => {
};
localStorageService.updateAnonymousCounter(id as string, updated);
loadAnonymousCounters();
console.log('Increment completed, reloading counters...');
await loadCounters(); // Refresh the list
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');
}
};
const searchCounters = (search: string) => {
if (isAuthenticated) {
loadCounters(search);
} else {
// Filter anonymous counters locally
const filtered = localStorageService.getAnonymousCounters().filter(counter =>
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),
return Object.entries(counter.entries || {}).map(([date, value], index) => ({
id: index + 1,
counter_id: parseInt(counter.id as string),
value: value as number,
date: date,
created_at: counter.updated_at,
}));
}
};
const searchCounters = (search: string) => {
loadCounters(search);
};
return {
counters: getDisplayCounters(),
counters,
isLoading,
error,
version, // Add version to force re-renders
createCounter,
updateCounter,
deleteCounter,
incrementCounter,
getCounterEntries,
searchCounters,
refreshCounters: isAuthenticated ? loadCounters : loadAnonymousCounters,
refreshCounters: loadCounters,
};
};

View File

@@ -23,7 +23,12 @@ export interface CounterEntry {
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;
today_value: number;
week_value: number;