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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</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/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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { useApp } from '../context/AppContext.jsx'
|
||||
import { useApp } from '../context/AppContext'
|
||||
|
||||
export function Navbar() {
|
||||
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 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(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<AppProvider>
|
||||
|
|
@ -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
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -119,5 +124,5 @@ export function Cart() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
<p className="text-gray-400">Safe and secure payment processing</p>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<OrderData[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(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<string, string> = {
|
||||
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() {
|
|||
<div className="text-xl">
|
||||
<span className="text-gray-400">Total:</span>{' '}
|
||||
<span className="text-white font-bold">
|
||||
${parseFloat(order.total_amount).toFixed(2)}
|
||||
${order.total_amount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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<ProductData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { addToCart } = useApp()
|
||||
const { getProducts } = useApi()
|
||||
|
|
@ -63,7 +64,16 @@ export function Products() {
|
|||
</span>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
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',
|
||||
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