forked from hazem/yslootahrobotics
- Removed the previous demo payment system and implemented a real integration with Stripe for handling payments. - Added new API routes for creating payment intents and handling webhooks from Stripe. - Updated the database schema to include an Order model for storing payment details. - Enhanced the admin page to manage pricing items, including the ability to upload 3D models. - Introduced a settings management feature for the admin panel, allowing for dynamic key-value pairs. - Improved the RobotModel component to support dynamic attire based on uploaded models. - Added error handling and validation for file uploads and settings management.
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import { createStore } from 'zustand/vanilla';
|
|
import { useSyncExternalStore } from 'react';
|
|
|
|
export interface PricingItem {
|
|
id: string;
|
|
label: string;
|
|
price: number;
|
|
modelPath?: string;
|
|
}
|
|
|
|
export interface PricingState {
|
|
items: PricingItem[];
|
|
isHydrated: boolean;
|
|
}
|
|
|
|
export interface PricingActions {
|
|
updatePrice: (itemId: string, newPrice: number) => void;
|
|
updateItem: (itemId: string, updates: Partial<Pick<PricingItem, 'label' | 'price' | 'modelPath'>>) => void;
|
|
addItem: (item: PricingItem) => void;
|
|
removeItem: (itemId: string) => void;
|
|
resetPrices: () => void;
|
|
hydrate: () => void;
|
|
}
|
|
|
|
export type PricingStore = PricingState & PricingActions;
|
|
|
|
const DEFAULT_ITEMS: PricingItem[] = [
|
|
{ id: 'base', label: 'G1 Robot Base', price: 250000 },
|
|
{ id: 'emarati-kandura', label: 'Emarati Kandura', price: 15000 },
|
|
{ id: 'industrial-vest', label: 'Industrial Vest', price: 8500 },
|
|
{ id: 'business-suit', label: 'Business Suit', price: 12000 },
|
|
{ id: 'custom-color', label: 'Custom Color', price: 3500 },
|
|
];
|
|
|
|
const STORAGE_KEY = 'lootah-pricing';
|
|
|
|
function loadFromStorage(): PricingItem[] | null {
|
|
if (typeof window === 'undefined') return null;
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw);
|
|
if (!Array.isArray(parsed)) return null;
|
|
return parsed;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function saveToStorage(items: PricingItem[]) {
|
|
if (typeof window === 'undefined') return;
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
|
} catch {
|
|
// Storage full or unavailable
|
|
}
|
|
}
|
|
|
|
export const pricingStore = createStore<PricingStore>((set, get) => ({
|
|
items: DEFAULT_ITEMS,
|
|
isHydrated: false,
|
|
|
|
updatePrice: (itemId: string, newPrice: number) => {
|
|
set((state) => {
|
|
const updated = state.items.map((item) =>
|
|
item.id === itemId ? { ...item, price: newPrice } : item
|
|
);
|
|
saveToStorage(updated);
|
|
return { items: updated };
|
|
});
|
|
},
|
|
|
|
updateItem: (itemId: string, updates: Partial<Pick<PricingItem, 'label' | 'price' | 'modelPath'>>) => {
|
|
set((state) => {
|
|
const updated = state.items.map((item) =>
|
|
item.id === itemId ? { ...item, ...updates } : item
|
|
);
|
|
saveToStorage(updated);
|
|
return { items: updated };
|
|
});
|
|
},
|
|
|
|
resetPrices: () => {
|
|
saveToStorage(DEFAULT_ITEMS);
|
|
set({ items: [...DEFAULT_ITEMS] });
|
|
},
|
|
|
|
addItem: (item: PricingItem) => {
|
|
set((state) => {
|
|
if (state.items.some((i) => i.id === item.id)) return state;
|
|
const updated = [...state.items, item];
|
|
saveToStorage(updated);
|
|
return { items: updated };
|
|
});
|
|
},
|
|
|
|
removeItem: (itemId: string) => {
|
|
// Prevent removing the base robot price
|
|
if (itemId === 'base') return;
|
|
set((state) => {
|
|
const updated = state.items.filter((i) => i.id !== itemId);
|
|
saveToStorage(updated);
|
|
return { items: updated };
|
|
});
|
|
},
|
|
|
|
hydrate: () => {
|
|
const stored = loadFromStorage();
|
|
if (stored && stored.length > 0) {
|
|
// Use stored items directly (preserves custom labels, prices, added items).
|
|
// Re-add any default items that were never stored (fresh install gap).
|
|
const storedIds = new Set(stored.map((s) => s.id));
|
|
const missing = DEFAULT_ITEMS.filter((d) => !storedIds.has(d.id));
|
|
set({ items: [...stored, ...missing], isHydrated: true });
|
|
} else {
|
|
set({ isHydrated: true });
|
|
}
|
|
},
|
|
}));
|
|
|
|
export const usePricingStore = <T>(selector: (state: PricingStore) => T): T => {
|
|
return useSyncExternalStore(
|
|
pricingStore.subscribe,
|
|
() => selector(pricingStore.getState()),
|
|
() => selector(pricingStore.getState())
|
|
);
|
|
};
|
|
|
|
export const getPrice = (itemId: string): number => {
|
|
const item = pricingStore.getState().items.find((i) => i.id === itemId);
|
|
return item?.price ?? 0;
|
|
};
|