feat: add useSettings hook for admin price management

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 12:59:02 +03:00
parent 76b9913b09
commit 52b5b77bc4
2 changed files with 117 additions and 0 deletions
@@ -0,0 +1,71 @@
import { renderHook, act } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
const { mockIn, mockEq } = vi.hoisted(() => ({
mockIn: vi.fn(),
mockEq: vi.fn(),
}));
vi.mock('../../../lib/supabase', () => ({
supabase: {
from: vi.fn(() => ({
select: vi.fn(() => ({ in: mockIn })),
update: vi.fn(() => ({ eq: mockEq })),
})),
},
}));
import { useSettings } from '../useSettings';
describe('useSettings', () => {
beforeEach(() => vi.clearAllMocks());
it('loads prices on mount', async () => {
mockIn.mockResolvedValue({
data: [{ key: 'price_200', value: '2000' }, { key: 'price_300', value: '3000' }],
error: null,
});
const { result } = renderHook(() => useSettings());
await act(async () => {});
expect(result.current.loading).toBe(false);
expect(result.current.prices).toEqual({ 200: 2000, 300: 3000 });
});
it('sets error when fetch fails', async () => {
mockIn.mockResolvedValue({ data: null, error: { message: 'DB error' } });
const { result } = renderHook(() => useSettings());
await act(async () => {});
expect(result.current.error).toBe('DB error');
});
it('updatePrice calls update().eq() and updates local state', async () => {
mockIn.mockResolvedValue({
data: [{ key: 'price_200', value: '2000' }, { key: 'price_300', value: '3000' }],
error: null,
});
mockEq.mockResolvedValue({ error: null });
const { result } = renderHook(() => useSettings());
await act(async () => {});
await act(async () => {
await result.current.updatePrice('price_200', 1800);
});
expect(mockEq).toHaveBeenCalledWith('key', 'price_200');
expect(result.current.prices[200]).toBe(1800);
});
it('updatePrice returns error without mutating state when DB fails', async () => {
mockIn.mockResolvedValue({
data: [{ key: 'price_200', value: '2000' }, { key: 'price_300', value: '3000' }],
error: null,
});
mockEq.mockResolvedValue({ error: { message: 'update failed' } });
const { result } = renderHook(() => useSettings());
await act(async () => {});
let updateResult;
await act(async () => {
updateResult = await result.current.updatePrice('price_200', 9999);
});
expect(updateResult.error.message).toBe('update failed');
expect(result.current.prices[200]).toBe(2000);
});
});
+46
View File
@@ -0,0 +1,46 @@
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '../../lib/supabase';
export function useSettings() {
const [prices, setPrices] = useState({ 200: 2000, 300: 3000 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchPrices = useCallback(async () => {
const { data, error } = await supabase
.from('settings')
.select('key, value')
.in('key', ['price_200', 'price_300']);
if (error) {
setError(error.message);
} else if (data) {
const map = {};
data.forEach(({ key, value }) => {
const thickness = parseInt(key.replace('price_', ''), 10);
map[thickness] = Number(value);
});
setPrices((prev) => ({ ...prev, ...map }));
}
setLoading(false);
}, []);
useEffect(() => {
fetchPrices();
}, [fetchPrices]);
const updatePrice = async (key, value) => {
const { error } = await supabase
.from('settings')
.update({ value: String(value), updated_at: new Date().toISOString() })
.eq('key', key);
if (!error) {
const thickness = parseInt(key.replace('price_', ''), 10);
setPrices((prev) => ({ ...prev, [thickness]: Number(value) }));
}
return { error };
};
return { prices, loading, error, updatePrice };
}