diff --git a/frontend/index.html b/frontend/index.html index 53876c3..2e13e82 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,6 +9,6 @@
- + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dded520..9f3290c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,8 +17,8 @@ "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.2.1", "@vitest/ui": "^1.0.4", "autoprefixer": "^10.4.16", @@ -29,6 +29,7 @@ "jsdom": "^23.0.1", "postcss": "^8.4.32", "tailwindcss": "^3.4.0", + "typescript": "^5.9.3", "vite": "^5.0.8", "vitest": "^1.0.4" } @@ -6488,6 +6489,19 @@ "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": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 798e5b4..0014018 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,28 +11,29 @@ "test:ui": "vitest --ui" }, "dependencies": { + "axios": "^1.6.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.20.1", - "axios": "^1.6.2" + "react-router-dom": "^6.20.1" }, "devDependencies": { - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", + "@testing-library/jest-dom": "^6.1.5", + "@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", - "vite": "^5.0.8", + "@vitest/ui": "^1.0.4", + "autoprefixer": "^10.4.16", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "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", - "tailwindcss": "^3.4.0", "postcss": "^8.4.32", - "autoprefixer": "^10.4.16" + "tailwindcss": "^3.4.0", + "typescript": "^5.9.3", + "vite": "^5.0.8", + "vitest": "^1.0.4" } -} \ No newline at end of file +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx deleted file mode 100644 index a2ac454..0000000 --- a/frontend/src/App.jsx +++ /dev/null @@ -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 ( -
- -
- - } /> - } /> - } /> - } /> - } /> - } /> - -
-
- ) -} - -export default App \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..cd9b4a0 --- /dev/null +++ b/frontend/src/App.tsx @@ -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 ( + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + +
+ +
+
+ ) +} + +export default App \ No newline at end of file diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index f8ae332..149097e 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom' -import { useApp } from '../context/AppContext.jsx' +import { useApp } from '../context/AppContext' export function Navbar() { const { user } = useApp() diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..149097e --- /dev/null +++ b/frontend/src/components/Navbar.tsx @@ -0,0 +1,70 @@ +import { Link } from 'react-router-dom' +import { useApp } from '../context/AppContext' + +export function Navbar() { + const { user } = useApp() + + return ( + + ) +} \ No newline at end of file diff --git a/frontend/src/context/AppContext.jsx b/frontend/src/context/AppContext.jsx deleted file mode 100644 index 1950823..0000000 --- a/frontend/src/context/AppContext.jsx +++ /dev/null @@ -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 ( - - {children} - - ) -} - -export function useApp() { - const context = useContext(AppContext) - if (!context) { - throw new Error('useApp must be used within an AppProvider') - } - return context -} \ No newline at end of file diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx new file mode 100644 index 0000000..36575bf --- /dev/null +++ b/frontend/src/context/AppContext.tsx @@ -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(undefined); + +// Export the context itself so components can use it with useContext +export { AppContext }; + +export function AppProvider({ children }: AppProviderProps) { + 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: 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 ( + + {children} + + ); +} + +export function useApp(): AppContextValue { + const context = useContext(AppContext); + if (!context) { + throw new Error('useApp must be used within an AppProvider'); + } + return context; +} diff --git a/frontend/src/context/modals/ModalComponents.tsx b/frontend/src/context/modals/ModalComponents.tsx new file mode 100644 index 0000000..21c222a --- /dev/null +++ b/frontend/src/context/modals/ModalComponents.tsx @@ -0,0 +1,29 @@ +import { ReactNode } from 'react'; + +// Container for the Header section +export const ModalHeader = ({ children, title }: { children?: ReactNode; title?: string }) => ( +
+ {title ? : children} +
+); + +// Container for the Main Body +export const ModalContent = ({ children }: { children: ReactNode }) => ( +
+ {children} +
+); + +// Container for Actions (Buttons) +export const ModalActions = ({ children }: { children: ReactNode }) => ( +
+ {children} +
+); + +// Helper to compose them easily if needed, or use individually +export const Modal = { + Header: ModalHeader, + Content: ModalContent, + Actions: ModalActions, +}; \ No newline at end of file diff --git a/frontend/src/context/modals/ModalExample.tsx b/frontend/src/context/modals/ModalExample.tsx new file mode 100644 index 0000000..3bbc34f --- /dev/null +++ b/frontend/src/context/modals/ModalExample.tsx @@ -0,0 +1,50 @@ +import { useModal } from './useModal'; +import { Modal } from './ModalComponents'; + +const DeleteConfirmModal = ({ onClose }: { onClose: () => void }) => { + return ( + <> + + +

+ Are you sure you want to delete this item? This action cannot be undone. +

+
+ + + + + + ); +}; + +export const ModalExample = () => { + const { openModal } = useModal(); + + const handleDeleteClick = () => { + // Pass the component factory to the modal + openModal((props) => ); + }; + + return ( +
+

Modal System Example

+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/context/modals/ModalRoot.tsx b/frontend/src/context/modals/ModalRoot.tsx new file mode 100644 index 0000000..c98ed0f --- /dev/null +++ b/frontend/src/context/modals/ModalRoot.tsx @@ -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( +
+ {/* Stop propagation so clicking modal content doesn't close it */} +
e.stopPropagation()} + > + {content({ onClose: closeModal })} +
+
, + document.body + ); +}; diff --git a/frontend/src/context/modals/useModal.tsx b/frontend/src/context/modals/useModal.tsx new file mode 100644 index 0000000..63c4515 --- /dev/null +++ b/frontend/src/context/modals/useModal.tsx @@ -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(undefined); + +interface ModalProviderProps { + children: ReactNode; +} + +export const ModalProvider: FC = ({ children }) => { + const [modalState, setModalState] = useState({ + isOpen: false, + content: null, + }); + + const openModal = useCallback((content: (props: ModalContentProps) => ReactNode) => { + setModalState({ isOpen: true, content }); + }, []); + + const closeModal = useCallback(() => { + setModalState((prev) => ({ ...prev, isOpen: false })); + }, []); + + return ( + + {children} + + ); +}; + +export const useModal = () => { + const context = useContext(ModalContext); + if (!context) { + throw new Error('useModal must be used within a ModalProvider'); + } + return context; +}; diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts new file mode 100644 index 0000000..887f383 --- /dev/null +++ b/frontend/src/hooks/useApi.ts @@ -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 => { + const response = await api.post('/auth/login', { email, password }) + return response.data + }, + register: async (userData: RegisterData): Promise => { + const response = await api.post('/auth/register', userData) + return response.data + }, + getCurrentUser: async (): Promise => { + const response = await api.get('/users/me') + return response.data + }, + + // Products + getProducts: async (): Promise => { + const response = await api.get('/products') + return response.data + }, + getProduct: async (id: string): Promise => { + const response = await api.get(`/products/${id}`) + return response.data + }, + createProduct: async (productData: Omit): Promise => { + const response = await api.post('/products', productData) + return response.data + }, + updateProduct: async (id: string, productData: Partial): Promise => { + const response = await api.put(`/products/${id}`, productData) + return response.data + }, + deleteProduct: async (id: string): Promise => { + await api.delete(`/products/${id}`) + }, + + // Orders + getOrders: async (): Promise => { + const response = await api.get('/orders') + return response.data + }, + getOrder: async (id: string): Promise => { + const response = await api.get(`/orders/${id}`) + return response.data + }, + createOrder: async (orderData: Omit): Promise => { + const response = await api.post('/orders', orderData) + return response.data + }, + } +} \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.tsx similarity index 66% rename from frontend/src/main.jsx rename to frontend/src/main.tsx index 95b926c..0889f69 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.tsx @@ -1,11 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' -import { AppProvider } from './context/AppContext.jsx' -import App from './App.jsx' +import { AppProvider } from './context/AppContext' +import App from './App.tsx' import './index.css' -ReactDOM.createRoot(document.getElementById('root')).render( +ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/Cart.jsx b/frontend/src/pages/Cart.tsx similarity index 82% rename from frontend/src/pages/Cart.jsx rename to frontend/src/pages/Cart.tsx index 767093b..457fed2 100644 --- a/frontend/src/pages/Cart.jsx +++ b/frontend/src/pages/Cart.tsx @@ -1,32 +1,37 @@ -import { useNavigate } from 'react-router-dom' -import { useApp } from '../context/AppContext.jsx' -import { useApi } from '../hooks/useApi.js' +import { useNavigate } from 'react-router-dom'; +import { useApp } from '../context/AppContext'; +import { useApi } from '../hooks/useApi'; -export function Cart() { - const navigate = useNavigate() - const { cart, removeFromCart, updateCartQuantity, clearCart, cartTotal } = useApp() - const { createOrder } = useApi() +export default function Cart() { + const navigate = useNavigate(); + const { cart, removeFromCart, updateCartQuantity, clearCart, cartTotal } = useApp(); + const { createOrder } = useApi(); const handleCheckout = async () => { - if (cart.length === 0) return + if (cart.length === 0) return; - const shippingAddress = prompt('Enter shipping address:') - if (!shippingAddress) return + const shippingAddress = prompt('Enter shipping address:'); + if (!shippingAddress) return; try { await createOrder({ items: cart.map((item) => ({ - product_id: item.id, + id: item.id.toString(), + product_id: item.id.toString(), quantity: item.quantity, + price: item.price, })), + status: 'pending', + total_amount: cartTotal, + created_at: new Date().toISOString(), shipping_address: shippingAddress, - }) - clearCart() - navigate('/orders') + }); + clearCart(); + navigate('/orders'); } catch (error) { - alert('Failed to create order. Please try again.') + alert('Failed to create order. Please try again.'); } - } + }; if (cart.length === 0) { return ( @@ -40,7 +45,7 @@ export function Cart() { Browse Products - ) + ); } return ( @@ -96,7 +101,7 @@ export function Cart() { ))} - +
Total:{' '} @@ -119,5 +124,5 @@ export function Cart() {
- ) + ); } \ No newline at end of file diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.tsx similarity index 77% rename from frontend/src/pages/Home.jsx rename to frontend/src/pages/Home.tsx index 6d2e7f8..73e56dd 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom' +import { ModalExample } from '../context/modals/ModalExample' export function Home() { return ( @@ -32,6 +33,14 @@ export function Home() {

Safe and secure payment processing

+ +
+

Modal System Demo

+

+ Test our modal system with this interactive example. The modal uses React Context for state management. +

+ +
) -} \ No newline at end of file +} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.tsx similarity index 82% rename from frontend/src/pages/Login.jsx rename to frontend/src/pages/Login.tsx index 5cae8c6..b48babc 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.tsx @@ -1,9 +1,10 @@ import { useState } from 'react' import { useNavigate, Link } from 'react-router-dom' -import { useApp } from '../context/AppContext.jsx' -import { useApi } from '../hooks/useApi.js' +import { useApp } from '../context/AppContext' +import { useApi } from '../hooks/useApi' +import { User } from '../types' -export function Login() { +export default function Login() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') @@ -13,17 +14,23 @@ export function Login() { const { login } = useApp() const { login: loginApi } = useApi() - const handleSubmit = async (e) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') setLoading(true) try { 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('/') } catch (err) { - setError(err.response?.data?.error || 'Login failed. Please try again.') + setError(err instanceof Error ? err.message : 'Login failed. Please try again.') } finally { setLoading(false) } diff --git a/frontend/src/pages/Orders.jsx b/frontend/src/pages/Orders.tsx similarity index 91% rename from frontend/src/pages/Orders.jsx rename to frontend/src/pages/Orders.tsx index a1b573f..167c806 100644 --- a/frontend/src/pages/Orders.jsx +++ b/frontend/src/pages/Orders.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { useApp } from '../context/AppContext.jsx' -import { useApi } from '../hooks/useApi.js' +import { useApp } from '../context/AppContext' +import { useApi } from '../hooks/useApi' +import { OrderData } from '../types' export function Orders() { - const [orders, setOrders] = useState([]) - const [loading, setLoading] = useState(true) + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) const navigate = useNavigate() const { user } = useApp() const { getOrders } = useApi() @@ -29,8 +30,8 @@ export function Orders() { } } - const getStatusColor = (status) => { - const colors = { + const getStatusColor = (status: string): string => { + const colors: Record = { pending: 'bg-yellow-900 text-yellow-200 border-yellow-700', processing: 'bg-blue-900 text-blue-200 border-blue-700', shipped: 'bg-purple-900 text-purple-200 border-purple-700', @@ -115,7 +116,7 @@ export function Orders() {
Total:{' '} - ${parseFloat(order.total_amount).toFixed(2)} + ${order.total_amount}
diff --git a/frontend/src/pages/Products.jsx b/frontend/src/pages/Products.tsx similarity index 80% rename from frontend/src/pages/Products.jsx rename to frontend/src/pages/Products.tsx index 5c9047f..a77e66d 100644 --- a/frontend/src/pages/Products.jsx +++ b/frontend/src/pages/Products.tsx @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react' -import { useApp } from '../context/AppContext.jsx' -import { useApi } from '../hooks/useApi.js' +import { useApp } from '../context/AppContext' +import { useApi } from '../hooks/useApi' +import { ProductData, CartItem } from '../types' export function Products() { - const [products, setProducts] = useState([]) + const [products, setProducts] = useState([]) const [loading, setLoading] = useState(true) const { addToCart } = useApp() const { getProducts } = useApi() @@ -63,7 +64,16 @@ export function Products() { + + +

+ Already have an account?{' '} + + Login + +

+ + ) +} \ No newline at end of file diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..643ce90 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,4 @@ +export interface ApiResponse { + data: T; + message?: string; +} \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..07f4990 --- /dev/null +++ b/frontend/src/types/index.ts @@ -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'; diff --git a/frontend/src/types/modal.ts b/frontend/src/types/modal.ts new file mode 100644 index 0000000..03feb3e --- /dev/null +++ b/frontend/src/types/modal.ts @@ -0,0 +1,3 @@ +export interface ModalContentProps { + onClose: () => void; +} \ No newline at end of file diff --git a/frontend/src/types/order.ts b/frontend/src/types/order.ts new file mode 100644 index 0000000..55eff94 --- /dev/null +++ b/frontend/src/types/order.ts @@ -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[]; +} \ No newline at end of file diff --git a/frontend/src/types/product.ts b/frontend/src/types/product.ts new file mode 100644 index 0000000..9f60b61 --- /dev/null +++ b/frontend/src/types/product.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts new file mode 100644 index 0000000..6a1b3a5 --- /dev/null +++ b/frontend/src/types/user.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1a7cc11 --- /dev/null +++ b/frontend/tsconfig.json @@ -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" }] +} \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.ts similarity index 82% rename from frontend/vite.config.js rename to frontend/vite.config.ts index 4308c30..34243ce 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.ts @@ -21,9 +21,4 @@ export default defineConfig({ outDir: 'dist', sourcemap: true, }, - test: { - globals: true, - environment: 'jsdom', - setupFiles: './src/setupTests.js', - }, }) \ No newline at end of file diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..ee78923 --- /dev/null +++ b/frontend/vitest.config.ts @@ -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', + }, +}) \ No newline at end of file