add modal and typescript integration
This commit is contained in:
parent
14a2b45deb
commit
f630ca6d69
31 changed files with 969 additions and 220 deletions
|
|
@ -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>
|
||||||
18
frontend/package-lock.json
generated
18
frontend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
33
frontend/src/App.tsx
Normal 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
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
70
frontend/src/components/Navbar.tsx
Normal file
70
frontend/src/components/Navbar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
154
frontend/src/context/AppContext.tsx
Normal file
154
frontend/src/context/AppContext.tsx
Normal 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;
|
||||||
|
}
|
||||||
29
frontend/src/context/modals/ModalComponents.tsx
Normal file
29
frontend/src/context/modals/ModalComponents.tsx
Normal 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,
|
||||||
|
};
|
||||||
50
frontend/src/context/modals/ModalExample.tsx
Normal file
50
frontend/src/context/modals/ModalExample.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
47
frontend/src/context/modals/ModalRoot.tsx
Normal file
47
frontend/src/context/modals/ModalRoot.tsx
Normal 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
|
||||||
|
);
|
||||||
|
};
|
||||||
48
frontend/src/context/modals/useModal.tsx
Normal file
48
frontend/src/context/modals/useModal.tsx
Normal 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;
|
||||||
|
};
|
||||||
95
frontend/src/hooks/useApi.ts
Normal file
95
frontend/src/hooks/useApi.ts
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 (
|
||||||
|
|
@ -96,7 +101,7 @@ export function Cart() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-between items-center">
|
<div className="mt-6 flex justify-between items-center">
|
||||||
<div className="text-xl">
|
<div className="text-xl">
|
||||||
<span className="text-gray-400">Total:</span>{' '}
|
<span className="text-gray-400">Total:</span>{' '}
|
||||||
|
|
@ -119,5 +124,5 @@ export function Cart() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
186
frontend/src/pages/Register.tsx
Normal file
186
frontend/src/pages/Register.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
frontend/src/types/api.ts
Normal file
4
frontend/src/types/api.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
14
frontend/src/types/index.ts
Normal file
14
frontend/src/types/index.ts
Normal 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';
|
||||||
3
frontend/src/types/modal.ts
Normal file
3
frontend/src/types/modal.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ModalContentProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
24
frontend/src/types/order.ts
Normal file
24
frontend/src/types/order.ts
Normal 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[];
|
||||||
|
}
|
||||||
26
frontend/src/types/product.ts
Normal file
26
frontend/src/types/product.ts
Normal 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;
|
||||||
|
}
|
||||||
30
frontend/src/types/user.ts
Normal file
30
frontend/src/types/user.ts
Normal 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
31
frontend/tsconfig.json
Normal 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" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -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
11
frontend/vitest.config.ts
Normal 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',
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue