From 52b5b77bc49e1aee9e8dc5dca6404fa43d7b5409 Mon Sep 17 00:00:00 2001 From: houseassassin Date: Sun, 10 May 2026 12:59:02 +0300 Subject: [PATCH] feat: add useSettings hook for admin price management Co-Authored-By: Claude Sonnet 4.6 --- .../settings/__tests__/useSettings.test.js | 71 +++++++++++++++++++ src/admin/settings/useSettings.js | 46 ++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/admin/settings/__tests__/useSettings.test.js create mode 100644 src/admin/settings/useSettings.js diff --git a/src/admin/settings/__tests__/useSettings.test.js b/src/admin/settings/__tests__/useSettings.test.js new file mode 100644 index 0000000..0455ed2 --- /dev/null +++ b/src/admin/settings/__tests__/useSettings.test.js @@ -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); + }); +}); diff --git a/src/admin/settings/useSettings.js b/src/admin/settings/useSettings.js new file mode 100644 index 0000000..2a5c040 --- /dev/null +++ b/src/admin/settings/useSettings.js @@ -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 }; +}