add modal and typescript integration

This commit is contained in:
david 2026-02-24 11:52:57 +03:00
parent 14a2b45deb
commit f630ca6d69
31 changed files with 969 additions and 220 deletions

View file

@ -9,6 +9,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View file

@ -17,8 +17,8 @@
"@testing-library/jest-dom": "^6.1.5", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2", "@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@types/react": "^18.2.43", "@types/react": "^18.3.28",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/ui": "^1.0.4", "@vitest/ui": "^1.0.4",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
@ -29,6 +29,7 @@
"jsdom": "^23.0.1", "jsdom": "^23.0.1",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.9.3",
"vite": "^5.0.8", "vite": "^5.0.8",
"vitest": "^1.0.4" "vitest": "^1.0.4"
} }
@ -6488,6 +6489,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ufo": { "node_modules/ufo": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",

View file

@ -11,28 +11,29 @@
"test:ui": "vitest --ui" "test:ui": "vitest --ui"
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1"
"axios": "^1.6.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.43", "@testing-library/jest-dom": "^6.1.5",
"@types/react-dom": "^18.2.17", "@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8", "@vitest/ui": "^1.0.4",
"autoprefixer": "^10.4.16",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"@testing-library/react": "^14.1.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/user-event": "^14.5.1",
"vitest": "^1.0.4",
"@vitest/ui": "^1.0.4",
"jsdom": "^23.0.1", "jsdom": "^23.0.1",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"autoprefixer": "^10.4.16" "tailwindcss": "^3.4.0",
"typescript": "^5.9.3",
"vite": "^5.0.8",
"vitest": "^1.0.4"
} }
} }

View file

@ -1,28 +0,0 @@
import { Routes, Route } from 'react-router-dom'
import { Navbar } from './components/Navbar'
import { Home } from './pages/Home'
import { Products } from './pages/Products'
import { Login } from './pages/Login'
import { Register } from './pages/Register'
import { Cart } from './pages/Cart'
import { Orders } from './pages/Orders'
function App() {
return (
<div className="min-h-screen bg-gray-900 text-gray-100">
<Navbar />
<main className="flex-1 p-8 max-w-7xl mx-auto w-full">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/cart" element={<Cart />} />
<Route path="/orders" element={<Orders />} />
</Routes>
</main>
</div>
)
}
export default App

33
frontend/src/App.tsx Normal file
View file

@ -0,0 +1,33 @@
import { Routes, Route } from 'react-router-dom'
import { ModalProvider } from './context/modals/useModal'
import { ModalRoot } from './context/modals/ModalRoot'
import Cart from './pages/Cart'
import { Navbar } from './components/Navbar'
import { Home } from './pages/Home'
import { Products } from './pages/Products'
import Login from './pages/Login'
import { Register } from './pages/Register'
import { Orders } from './pages/Orders'
const App = () => {
return (
<ModalProvider>
<div className="min-h-screen bg-gray-900 text-gray-100">
<Navbar />
<main className="flex-1 p-8 max-w-7xl mx-auto w-full">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/cart" element={<Cart />} />
<Route path="/orders" element={<Orders />} />
</Routes>
</main>
<ModalRoot />
</div>
</ModalProvider>
)
}
export default App

View file

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useApp } from '../context/AppContext.jsx' import { useApp } from '../context/AppContext'
export function Navbar() { export function Navbar() {
const { user } = useApp() const { user } = useApp()

View file

@ -0,0 +1,70 @@
import { Link } from 'react-router-dom'
import { useApp } from '../context/AppContext'
export function Navbar() {
const { user } = useApp()
return (
<nav className="bg-gray-800 border-b border-gray-700 shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<Link to="/" className="text-xl font-bold text-white hover:text-blue-400 transition-colors">
Crafting Shop
</Link>
<div className="ml-10 flex items-baseline space-x-4">
<Link
to="/"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Home
</Link>
<Link
to="/products"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Products
</Link>
<Link
to="/cart"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Cart
</Link>
{user && (
<Link
to="/orders"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Orders
</Link>
)}
</div>
</div>
<div className="flex items-center">
{user ? (
<span className="text-gray-300 px-3 py-2">
{user.username}
</span>
) : (
<>
<Link
to="/login"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Login
</Link>
<Link
to="/register"
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
>
Register
</Link>
</>
)}
</div>
</div>
</div>
</nav>
)
}

View file

@ -1,130 +0,0 @@
import { createContext, useContext, useState, useEffect } from 'react'
const AppContext = createContext()
export function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [token, setToken] = useState(null)
const [cart, setCart] = useState([])
const [loading, setLoading] = useState(true)
// Load user and token from localStorage on mount
useEffect(() => {
const storedToken = localStorage.getItem('token')
const storedUser = localStorage.getItem('user')
if (storedToken && storedUser) {
setToken(storedToken)
setUser(JSON.parse(storedUser))
}
const storedCart = localStorage.getItem('cart')
if (storedCart) {
setCart(JSON.parse(storedCart))
}
setLoading(false)
}, [])
// Save to localStorage whenever user, token, or cart changes
useEffect(() => {
if (user) {
localStorage.setItem('user', JSON.stringify(user))
} else {
localStorage.removeItem('user')
}
}, [user])
useEffect(() => {
if (token) {
localStorage.setItem('token', token)
} else {
localStorage.removeItem('token')
}
}, [token])
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart))
}, [cart])
const login = (userData, authToken) => {
setUser(userData)
setToken(authToken)
}
const logout = () => {
setUser(null)
setToken(null)
setCart([])
}
const addToCart = (product) => {
setCart((prevCart) => {
const existingItem = prevCart.find((item) => item.id === product.id)
if (existingItem) {
return prevCart.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
return [...prevCart, { ...product, quantity: 1 }]
})
}
const removeFromCart = (productId) => {
setCart((prevCart) => prevCart.filter((item) => item.id !== productId))
}
const updateCartQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId)
return
}
setCart((prevCart) =>
prevCart.map((item) =>
item.id === productId ? { ...item, quantity } : item
)
)
}
const clearCart = () => {
setCart([])
}
const cartTotal = cart.reduce(
(total, item) => total + item.price * item.quantity,
0
)
const cartItemCount = cart.reduce((total, item) => total + item.quantity, 0)
return (
<AppContext.Provider
value={{
user,
token,
cart,
loading,
login,
logout,
addToCart,
removeFromCart,
updateCartQuantity,
clearCart,
cartTotal,
cartItemCount,
}}
>
{children}
</AppContext.Provider>
)
}
export function useApp() {
const context = useContext(AppContext)
if (!context) {
throw new Error('useApp must be used within an AppProvider')
}
return context
}

View file

@ -0,0 +1,154 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { User, CartItem } from '../types';
interface AppContextValue {
user: User | null;
token: string | null;
cart: CartItem[];
loading: boolean;
login: (userData: User, authToken: string) => void;
logout: () => void;
addToCart: (product: CartItem) => void;
removeFromCart: (productId: number) => void;
updateCartQuantity: (productId: number, quantity: number) => void;
clearCart: () => void;
cartTotal: number;
cartItemCount: number;
}
interface AppProviderProps {
children: ReactNode;
}
// Create context with explicit type
const AppContext = createContext<AppContextValue | undefined>(undefined);
// Export the context itself so components can use it with useContext
export { AppContext };
export function AppProvider({ children }: AppProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [cart, setCart] = useState<CartItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// Load user and token from localStorage on mount
useEffect(() => {
const storedToken = localStorage.getItem('token');
const storedUser = localStorage.getItem('user');
if (storedToken && storedUser) {
setToken(storedToken);
setUser(JSON.parse(storedUser));
}
const storedCart = localStorage.getItem('cart');
if (storedCart) {
setCart(JSON.parse(storedCart));
}
setLoading(false);
}, []);
// Save to localStorage whenever user, token, or cart changes
useEffect(() => {
if (user) {
localStorage.setItem('user', JSON.stringify(user));
} else {
localStorage.removeItem('user');
}
}, [user]);
useEffect(() => {
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
}, [token]);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const login = (userData: User, authToken: string) => {
setUser(userData);
setToken(authToken);
};
const logout = () => {
setUser(null);
setToken(null);
setCart([]);
};
const addToCart = (product: CartItem) => {
setCart((prevCart: CartItem[]) => {
const existingItem = prevCart.find((item) => item.id === product.id);
if (existingItem) {
return prevCart.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prevCart, { ...product, quantity: 1 }];
});
};
const removeFromCart = (productId: number) => {
setCart((prevCart: CartItem[]) => prevCart.filter((item) => item.id !== productId));
};
const updateCartQuantity = (productId: number, quantity: number) => {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
setCart((prevCart: CartItem[]) =>
prevCart.map((item) =>
item.id === productId ? { ...item, quantity } : item
)
);
};
const clearCart = () => {
setCart([]);
};
const cartTotal = cart.reduce(
(total: number, item: CartItem) => total + item.price * item.quantity,
0
);
const cartItemCount = cart.reduce((total: number, item: CartItem) => total + item.quantity, 0);
return (
<AppContext.Provider
value={{
user,
token,
cart,
loading,
login,
logout,
addToCart,
removeFromCart,
updateCartQuantity,
clearCart,
cartTotal,
cartItemCount,
}}
>
{children}
</AppContext.Provider>
);
}
export function useApp(): AppContextValue {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
}

View file

@ -0,0 +1,29 @@
import { ReactNode } from 'react';
// Container for the Header section
export const ModalHeader = ({ children, title }: { children?: ReactNode; title?: string }) => (
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
{title ? <h3 id="modal-title" className="text-lg font-semibold text-gray-900">{title}</h3> : children}
</div>
);
// Container for the Main Body
export const ModalContent = ({ children }: { children: ReactNode }) => (
<div className="px-6 py-4 overflow-y-auto max-h-[60vh]">
{children}
</div>
);
// Container for Actions (Buttons)
export const ModalActions = ({ children }: { children: ReactNode }) => (
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3">
{children}
</div>
);
// Helper to compose them easily if needed, or use individually
export const Modal = {
Header: ModalHeader,
Content: ModalContent,
Actions: ModalActions,
};

View file

@ -0,0 +1,50 @@
import { useModal } from './useModal';
import { Modal } from './ModalComponents';
const DeleteConfirmModal = ({ onClose }: { onClose: () => void }) => {
return (
<>
<Modal.Header title="Delete Item" />
<Modal.Content>
<p className="text-gray-600">
Are you sure you want to delete this item? This action cannot be undone.
</p>
</Modal.Content>
<Modal.Actions>
<button
onClick={onClose}
className="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={() => { /* Delete logic here */ onClose(); }}
className="px-4 py-2 text-white bg-red-600 rounded hover:bg-red-700"
>
Delete
</button>
</Modal.Actions>
</>
);
};
export const ModalExample = () => {
const { openModal } = useModal();
const handleDeleteClick = () => {
// Pass the component factory to the modal
openModal((props) => <DeleteConfirmModal {...props} />);
};
return (
<div className="p-10">
<h1 className="text-2xl font-bold mb-4">Modal System Example</h1>
<button
onClick={handleDeleteClick}
className="px-6 py-3 text-white bg-blue-600 rounded hover:bg-blue-700"
>
Open Delete Modal
</button>
</div>
);
};

View file

@ -0,0 +1,47 @@
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useModal } from './useModal';
export const ModalRoot = () => {
const { isOpen, content, closeModal } = useModal();
// Handle Escape Key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) closeModal();
};
if (isOpen) window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, closeModal]);
// Prevent body scroll when modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => { document.body.style.overflow = 'unset'; };
}, [isOpen]);
if (!isOpen || !content) return null;
return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
onClick={closeModal} // Click outside to close
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
{/* Stop propagation so clicking modal content doesn't close it */}
<div
className="bg-white rounded-lg shadow-xl w-full max-w-md overflow-hidden flex flex-col transform transition-all"
onClick={(e) => e.stopPropagation()}
>
{content({ onClose: closeModal })}
</div>
</div>,
document.body
);
};

View file

@ -0,0 +1,48 @@
import { createContext, useContext, useState, useCallback, ReactNode, FC } from 'react';
import { ModalContentProps } from '../../types';
interface ModalState {
isOpen: boolean;
// We store a function that returns React Node, allowing us to pass props
content: ((props: ModalContentProps) => ReactNode) | null;
}
interface ModalContextType extends ModalState {
openModal: (content: (props: ModalContentProps) => ReactNode) => void;
closeModal: () => void;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
interface ModalProviderProps {
children: ReactNode;
}
export const ModalProvider: FC<ModalProviderProps> = ({ children }) => {
const [modalState, setModalState] = useState<ModalState>({
isOpen: false,
content: null,
});
const openModal = useCallback((content: (props: ModalContentProps) => ReactNode) => {
setModalState({ isOpen: true, content });
}, []);
const closeModal = useCallback(() => {
setModalState((prev) => ({ ...prev, isOpen: false }));
}, []);
return (
<ModalContext.Provider value={{ ...modalState, openModal, closeModal }}>
{children}
</ModalContext.Provider>
);
};
export const useModal = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModal must be used within a ModalProvider');
}
return context;
};

View file

@ -0,0 +1,95 @@
import axios from 'axios'
import {
RegisterData,
UserData,
ProductData,
OrderData,
AuthResponse
} from '../types'
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
})
// Add token to requests if available
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// Handle response errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export function useApi() {
return {
// Auth
login: async (email: string, password: string): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/login', { email, password })
return response.data
},
register: async (userData: RegisterData): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/register', userData)
return response.data
},
getCurrentUser: async (): Promise<UserData> => {
const response = await api.get<UserData>('/users/me')
return response.data
},
// Products
getProducts: async (): Promise<ProductData[]> => {
const response = await api.get<ProductData[]>('/products')
return response.data
},
getProduct: async (id: string): Promise<ProductData> => {
const response = await api.get<ProductData>(`/products/${id}`)
return response.data
},
createProduct: async (productData: Omit<ProductData, 'id'>): Promise<ProductData> => {
const response = await api.post<ProductData>('/products', productData)
return response.data
},
updateProduct: async (id: string, productData: Partial<ProductData>): Promise<ProductData> => {
const response = await api.put<ProductData>(`/products/${id}`, productData)
return response.data
},
deleteProduct: async (id: string): Promise<void> => {
await api.delete(`/products/${id}`)
},
// Orders
getOrders: async (): Promise<OrderData[]> => {
const response = await api.get<OrderData[]>('/orders')
return response.data
},
getOrder: async (id: string): Promise<OrderData> => {
const response = await api.get<OrderData>(`/orders/${id}`)
return response.data
},
createOrder: async (orderData: Omit<OrderData, 'id'>): Promise<OrderData> => {
const response = await api.post<OrderData>('/orders', orderData)
return response.data
},
}
}

View file

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { AppProvider } from './context/AppContext.jsx' import { AppProvider } from './context/AppContext'
import App from './App.jsx' import App from './App.tsx'
import './index.css' import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<AppProvider> <AppProvider>

View file

@ -1,32 +1,37 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import { useApp } from '../context/AppContext.jsx' import { useApp } from '../context/AppContext';
import { useApi } from '../hooks/useApi.js' import { useApi } from '../hooks/useApi';
export function Cart() { export default function Cart() {
const navigate = useNavigate() const navigate = useNavigate();
const { cart, removeFromCart, updateCartQuantity, clearCart, cartTotal } = useApp() const { cart, removeFromCart, updateCartQuantity, clearCart, cartTotal } = useApp();
const { createOrder } = useApi() const { createOrder } = useApi();
const handleCheckout = async () => { const handleCheckout = async () => {
if (cart.length === 0) return if (cart.length === 0) return;
const shippingAddress = prompt('Enter shipping address:') const shippingAddress = prompt('Enter shipping address:');
if (!shippingAddress) return if (!shippingAddress) return;
try { try {
await createOrder({ await createOrder({
items: cart.map((item) => ({ items: cart.map((item) => ({
product_id: item.id, id: item.id.toString(),
product_id: item.id.toString(),
quantity: item.quantity, quantity: item.quantity,
price: item.price,
})), })),
status: 'pending',
total_amount: cartTotal,
created_at: new Date().toISOString(),
shipping_address: shippingAddress, shipping_address: shippingAddress,
}) });
clearCart() clearCart();
navigate('/orders') navigate('/orders');
} catch (error) { } catch (error) {
alert('Failed to create order. Please try again.') alert('Failed to create order. Please try again.');
}
} }
};
if (cart.length === 0) { if (cart.length === 0) {
return ( return (
@ -40,7 +45,7 @@ export function Cart() {
Browse Products Browse Products
</button> </button>
</div> </div>
) );
} }
return ( return (
@ -119,5 +124,5 @@ export function Cart() {
</div> </div>
</div> </div>
</div> </div>
) );
} }

View file

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ModalExample } from '../context/modals/ModalExample'
export function Home() { export function Home() {
return ( return (
@ -32,6 +33,14 @@ export function Home() {
<p className="text-gray-400">Safe and secure payment processing</p> <p className="text-gray-400">Safe and secure payment processing</p>
</div> </div>
</div> </div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-2xl font-semibold text-white mb-4">Modal System Demo</h2>
<p className="text-gray-400 mb-6">
Test our modal system with this interactive example. The modal uses React Context for state management.
</p>
<ModalExample />
</div>
</div> </div>
) )
} }

View file

@ -1,9 +1,10 @@
import { useState } from 'react' import { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom' import { useNavigate, Link } from 'react-router-dom'
import { useApp } from '../context/AppContext.jsx' import { useApp } from '../context/AppContext'
import { useApi } from '../hooks/useApi.js' import { useApi } from '../hooks/useApi'
import { User } from '../types'
export function Login() { export default function Login() {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
@ -13,17 +14,23 @@ export function Login() {
const { login } = useApp() const { login } = useApp()
const { login: loginApi } = useApi() const { login: loginApi } = useApi()
const handleSubmit = async (e) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError('') setError('')
setLoading(true) setLoading(true)
try { try {
const response = await loginApi(email, password) const response = await loginApi(email, password)
login(response.user, response.access_token) // Convert UserData to User type
const user: User = {
id: parseInt(response.user.id),
username: response.user.username,
email: response.user.email,
}
login(user, response.token)
navigate('/') navigate('/')
} catch (err) { } catch (err) {
setError(err.response?.data?.error || 'Login failed. Please try again.') setError(err instanceof Error ? err.message : 'Login failed. Please try again.')
} finally { } finally {
setLoading(false) setLoading(false)
} }

View file

@ -1,11 +1,12 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useApp } from '../context/AppContext.jsx' import { useApp } from '../context/AppContext'
import { useApi } from '../hooks/useApi.js' import { useApi } from '../hooks/useApi'
import { OrderData } from '../types'
export function Orders() { export function Orders() {
const [orders, setOrders] = useState([]) const [orders, setOrders] = useState<OrderData[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState<boolean>(true)
const navigate = useNavigate() const navigate = useNavigate()
const { user } = useApp() const { user } = useApp()
const { getOrders } = useApi() const { getOrders } = useApi()
@ -29,8 +30,8 @@ export function Orders() {
} }
} }
const getStatusColor = (status) => { const getStatusColor = (status: string): string => {
const colors = { const colors: Record<string, string> = {
pending: 'bg-yellow-900 text-yellow-200 border-yellow-700', pending: 'bg-yellow-900 text-yellow-200 border-yellow-700',
processing: 'bg-blue-900 text-blue-200 border-blue-700', processing: 'bg-blue-900 text-blue-200 border-blue-700',
shipped: 'bg-purple-900 text-purple-200 border-purple-700', shipped: 'bg-purple-900 text-purple-200 border-purple-700',
@ -115,7 +116,7 @@ export function Orders() {
<div className="text-xl"> <div className="text-xl">
<span className="text-gray-400">Total:</span>{' '} <span className="text-gray-400">Total:</span>{' '}
<span className="text-white font-bold"> <span className="text-white font-bold">
${parseFloat(order.total_amount).toFixed(2)} ${order.total_amount}
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,9 +1,10 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useApp } from '../context/AppContext.jsx' import { useApp } from '../context/AppContext'
import { useApi } from '../hooks/useApi.js' import { useApi } from '../hooks/useApi'
import { ProductData, CartItem } from '../types'
export function Products() { export function Products() {
const [products, setProducts] = useState([]) const [products, setProducts] = useState<ProductData[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const { addToCart } = useApp() const { addToCart } = useApp()
const { getProducts } = useApi() const { getProducts } = useApi()
@ -63,7 +64,16 @@ export function Products() {
</span> </span>
</div> </div>
<button <button
onClick={() => addToCart(product)} onClick={() => {
const cartItem: CartItem = {
id: parseInt(product.id!),
name: product.name,
price: product.price,
quantity: 1,
image_url: product.image_url
};
addToCart(cartItem);
}}
className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors" className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors"
> >
Add to Cart Add to Cart

View file

@ -0,0 +1,186 @@
import { useState, FormEvent, ChangeEvent } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { useApi } from '../hooks/useApi'
interface FormData {
email: string
username: string
password: string
confirmPassword: string
first_name: string
last_name: string
}
export function Register() {
const [formData, setFormData] = useState<FormData>({
email: '',
username: '',
password: '',
confirmPassword: '',
first_name: '',
last_name: '',
})
const [error, setError] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const navigate = useNavigate()
const { register } = useApi()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
setError('')
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match')
return
}
if (formData.password.length < 6) {
setError('Password must be at least 6 characters')
return
}
setLoading(true)
try {
await register({
email: formData.email,
username: formData.username,
password: formData.password,
first_name: formData.first_name,
last_name: formData.last_name,
})
navigate('/login')
} catch (err: any) {
setError(err.response?.data?.error || 'Registration failed. Please try again.')
} finally {
setLoading(false)
}
}
return (
<div className="max-w-md mx-auto">
<h1 className="text-3xl font-bold text-white mb-8 text-center">Register</h1>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded">
{error}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="first_name" className="block text-sm font-medium text-gray-300 mb-2">
First Name
</label>
<input
id="first_name"
name="first_name"
type="text"
value={formData.first_name}
onChange={handleChange}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="last_name" className="block text-sm font-medium text-gray-300 mb-2">
Last Name
</label>
<input
id="last_name"
name="last_name"
type="text"
value={formData.last_name}
onChange={handleChange}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-300 mb-2">
Username
</label>
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-300 mb-2">
Confirm Password
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Registering...' : 'Register'}
</button>
</form>
<p className="mt-6 text-center text-gray-400">
Already have an account?{' '}
<Link to="/login" className="text-blue-400 hover:text-blue-300">
Login
</Link>
</p>
</div>
)
}

View file

@ -0,0 +1,4 @@
export interface ApiResponse<T> {
data: T;
message?: string;
}

View file

@ -0,0 +1,14 @@
// User types
export * from './user';
// Product types
export * from './product';
// Order types
export * from './order';
// API types
export * from './api';
// Modal types
export * from './modal';

View file

@ -0,0 +1,3 @@
export interface ModalContentProps {
onClose: () => void;
}

View file

@ -0,0 +1,24 @@
export interface OrderItem {
id: string;
product_id: string;
quantity: number;
price: number;
}
export interface OrderData {
id: string;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
total_amount: number;
created_at: string;
shipping_address?: string;
items: OrderItem[];
}
export interface Order {
id: number;
created_at: string;
status: string;
total_amount: number;
shipping_address: string;
items: OrderItem[];
}

View file

@ -0,0 +1,26 @@
export interface Product {
id: string;
name: string;
description: string;
price: number;
stock: number;
image_url?: string;
}
export interface ProductData {
id?: string;
name: string;
description: string;
price: number;
stock: number;
image_url?: string;
}
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
image_url?: string;
[key: string]: any;
}

View file

@ -0,0 +1,30 @@
export interface User {
id: number;
username: string;
email: string;
[key: string]: any;
}
export interface UserData {
id: string;
email: string;
username: string;
}
export interface LoginData {
email: string;
password: string;
}
export interface RegisterData {
email: string;
password: string;
username: string;
first_name: string;
last_name: string;
}
export interface AuthResponse {
token: string;
user: UserData;
}

31
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View file

@ -21,9 +21,4 @@ export default defineConfig({
outDir: 'dist', outDir: 'dist',
sourcemap: true, sourcemap: true,
}, },
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
},
}) })

11
frontend/vitest.config.ts Normal file
View file

@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.js',
},
})