diff --git a/src/App.tsx b/src/App.tsx
index 08c29fa..ce5992b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,11 @@ import ProductPage from './ProductPage'
import ProductsPage from './ProductsPage'
import CheckoutPage from './CheckoutPage'
import CheckoutSuccessPage from './CheckoutSuccessPage'
+import LoginPage from './LoginPage'
+import RegisterPage from './RegisterPage'
+import UserOrdersPage from './UserOrdersPage'
+import ProtectedRoute from './components/ProtectedRoute'
+import { AuthProvider, useAuth } from './contexts/AuthContext'
import { GET_PRODUCTS } from './graphql/queries'
const currency = new Intl.NumberFormat('en-US', {
@@ -126,6 +131,8 @@ function Layout({
toggleCart,
updateQuantity,
}: LayoutProps) {
+ const { isAuthenticated, user, logout } = useAuth()
+
return (
@@ -133,17 +140,54 @@ function Layout({
Your bag
{cartCount ? `${cartCount} item${cartCount > 1 ? 's' : ''}` : 'No items yet'}
-
+
+ {isAuthenticated ? (
+ <>
+
+ {user?.email}
+
+
+ >
+ ) : (
+
+ Login
+
+ )}
+
+
{isCartOpen && (
@@ -190,6 +234,7 @@ function Layout({
to="/checkout"
className="btn btn--primary btn--full"
style={{ textAlign: 'center', display: 'block', textDecoration: 'none' }}
+ onClick={toggleCart}
>
Proceed to checkout
@@ -513,8 +558,72 @@ function App() {
}
/>
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
)
}
-export default App
+function AppWithAuth() {
+ return (
+
+
+
+ )
+}
+
+export default AppWithAuth
diff --git a/src/CheckoutSuccessPage.tsx b/src/CheckoutSuccessPage.tsx
index d979c97..fe66870 100644
--- a/src/CheckoutSuccessPage.tsx
+++ b/src/CheckoutSuccessPage.tsx
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
-import { Link, useParams } from 'react-router-dom'
+import { useParams } from 'react-router-dom'
import { useQuery } from '@apollo/client/react'
import './App.css'
import { GET_CHECKOUT } from './graphql/queries'
@@ -77,9 +77,15 @@ export default function CheckoutSuccessPage({ onClearCart }: CheckoutSuccessPage
Checkout not found
Sorry, we couldn't find your checkout information.
-
+
@@ -130,9 +136,16 @@ export default function CheckoutSuccessPage({ onClearCart }: CheckoutSuccessPage
-
+
diff --git a/src/LoginPage.tsx b/src/LoginPage.tsx
new file mode 100644
index 0000000..d6c96da
--- /dev/null
+++ b/src/LoginPage.tsx
@@ -0,0 +1,101 @@
+import { useState, type FormEvent } from 'react'
+import { useNavigate, Link, useLocation } from 'react-router-dom'
+import { useAuth } from './contexts/AuthContext'
+import './App.css'
+
+export default function LoginPage() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const { login } = useAuth()
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ const from = location.state?.from?.pathname || '/account/orders'
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault()
+ setError('')
+ setLoading(true)
+
+ try {
+ await login(email, password)
+ navigate(from, { replace: true })
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Login failed. Please try again.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/src/ProductsPage.tsx b/src/ProductsPage.tsx
index da06564..eb0f97b 100644
--- a/src/ProductsPage.tsx
+++ b/src/ProductsPage.tsx
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
-import { Link } from 'react-router-dom'
+import { Link, useLocation } from 'react-router-dom'
import { useQuery } from '@apollo/client/react'
import './App.css'
import ProductCard from './ProductCard'
@@ -16,12 +16,13 @@ type ProductsQueryResult = {
}
export default function ProductsPage({ addToCart, isHighlighted }: ProductsPageProps) {
+ const location = useLocation()
const { loading, error, data } = useQuery(GET_PRODUCTS)
- // Scroll to top when products page loads
+ // Scroll to top when products page loads or location changes
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'smooth' })
- }, [])
+ }, [location.pathname])
if (loading) return Loading products...
if (error) return Error loading products: {error.message}
diff --git a/src/RegisterPage.tsx b/src/RegisterPage.tsx
new file mode 100644
index 0000000..f5b6679
--- /dev/null
+++ b/src/RegisterPage.tsx
@@ -0,0 +1,142 @@
+import { useState, type FormEvent } from 'react'
+import { useNavigate, Link } from 'react-router-dom'
+import { useAuth } from './contexts/AuthContext'
+import './App.css'
+
+export default function RegisterPage() {
+ const navigate = useNavigate()
+ const { register } = useAuth()
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [firstName, setFirstName] = useState('')
+ const [lastName, setLastName] = useState('')
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault()
+ setError('')
+
+ if (password.length < 6) {
+ setError('Password must be at least 6 characters long')
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ await register(email, password, firstName, lastName)
+ navigate('/account/orders', { replace: true })
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Registration failed. Please try again.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
diff --git a/src/UserOrdersPage.tsx b/src/UserOrdersPage.tsx
new file mode 100644
index 0000000..c68f1c5
--- /dev/null
+++ b/src/UserOrdersPage.tsx
@@ -0,0 +1,233 @@
+import { Link } from 'react-router-dom'
+import { useEffect } from 'react'
+import { useQuery } from '@apollo/client/react'
+import { useAuth } from './contexts/AuthContext'
+import { MY_ORDERS } from './graphql/queries'
+import './App.css'
+
+const currency = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+})
+
+type OrderItem = {
+ productId: string
+ name: string
+ quantity: number
+ price: number
+}
+
+type Order = {
+ id: string
+ status: string
+ total: number
+ items: OrderItem[]
+ paymentMethod: string
+ createdAt: string
+}
+
+type MyOrdersQueryResult = {
+ myOrders: Order[]
+}
+
+function getStatusColor(status: string): string {
+ switch (status) {
+ case 'completed':
+ return '#22c55e'
+ case 'processing':
+ return '#3b82f6'
+ case 'pending':
+ return '#f59e0b'
+ case 'failed':
+ return '#ef4444'
+ case 'cancelled':
+ return '#6b7280'
+ default:
+ return '#6b7280'
+ }
+}
+
+function getStatusLabel(status: string): string {
+ return status.charAt(0).toUpperCase() + status.slice(1)
+}
+
+export default function UserOrdersPage() {
+ const { isAuthenticated, token } = useAuth()
+ const { loading, error, data } = useQuery(MY_ORDERS, {
+ skip: !isAuthenticated,
+ errorPolicy: 'all',
+ })
+
+ // Debug: Log authentication state
+ useEffect(() => {
+ console.log(
+ 'UserOrdersPage - isAuthenticated:',
+ isAuthenticated,
+ 'token:',
+ token ? 'present' : 'missing',
+ )
+ }, [isAuthenticated, token])
+
+ if (loading) {
+ return (
+
+
+
+
Loading your orders...
+
+
+
+ )
+ }
+
+ if (error) {
+ console.error('Error loading orders:', error)
+ const apolloError = error as {
+ message: string
+ networkError?: { message: string }
+ graphQLErrors?: Array<{ message: string }>
+ }
+ return (
+
+
+
+
Error loading orders
+
{apolloError.message}
+ {apolloError.networkError && (
+
+ Network error: {apolloError.networkError.message}
+
+ )}
+ {apolloError.graphQLErrors && apolloError.graphQLErrors.length > 0 && (
+
+ {apolloError.graphQLErrors.map((err: { message: string }, idx: number) => (
+
+ {err.message}
+
+ ))}
+
+ )}
+
+ Continue Shopping
+
+
+
+
+ )
+ }
+
+ const orders = data?.myOrders || []
+
+ if (orders.length === 0) {
+ return (
+
+
+
+
No orders yet
+
You haven't placed any orders yet. Start shopping to see your orders here.
+
+ Start Shopping
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ ← Back to products
+
+
+
+
+
My Orders
+
+
+ {orders.map((order) => (
+
+
+
+
+ Order #{order.id.slice(0, 8)}
+
+
+ {new Date(order.createdAt).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+
+
+
+ {getStatusLabel(order.status)}
+
+
+ {currency.format(order.total)}
+
+
+
+
+
+
+ Items:
+
+
+ {order.items.map((item, index) => (
+ -
+ {item.name} × {item.quantity} -{' '}
+ {currency.format(item.price * item.quantity)}
+
+ ))}
+
+
+
+
+ View order details →
+
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..1f31513
--- /dev/null
+++ b/src/components/ProtectedRoute.tsx
@@ -0,0 +1,31 @@
+import { Navigate, useLocation } from 'react-router-dom'
+import { useAuth } from '../contexts/AuthContext'
+
+interface ProtectedRouteProps {
+ children: React.ReactNode
+}
+
+export default function ProtectedRoute({ children }: ProtectedRouteProps) {
+ const { isAuthenticated, loading } = useAuth()
+ const location = useLocation()
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ if (!isAuthenticated) {
+ // Redirect to login page, preserving the intended destination
+ return
+ }
+
+ return <>{children}>
+}
+
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..4ab3749
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,133 @@
+import { createContext, useContext, useState, type ReactNode } from 'react'
+
+interface User {
+ id: string
+ email: string
+ firstName: string
+ lastName: string
+}
+
+interface AuthContextType {
+ user: User | null
+ token: string | null
+ login: (email: string, password: string) => Promise
+ register: (email: string, password: string, firstName: string, lastName: string) => Promise
+ logout: () => void
+ isAuthenticated: boolean
+ loading: boolean
+}
+
+const AuthContext = createContext(undefined)
+
+const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'
+
+// Initialize state from localStorage
+function getInitialAuthState() {
+ const storedToken = localStorage.getItem('auth_token')
+ const storedUser = localStorage.getItem('auth_user')
+
+ if (storedToken && storedUser) {
+ try {
+ return {
+ token: storedToken,
+ user: JSON.parse(storedUser) as User,
+ loading: false,
+ }
+ } catch (error) {
+ console.error('Error parsing stored user:', error)
+ localStorage.removeItem('auth_token')
+ localStorage.removeItem('auth_user')
+ }
+ }
+
+ return {
+ token: null,
+ user: null,
+ loading: false,
+ }
+}
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const initialState = getInitialAuthState()
+ const [user, setUser] = useState(initialState.user)
+ const [token, setToken] = useState(initialState.token)
+ const [loading] = useState(initialState.loading)
+
+ const login = async (email: string, password: string) => {
+ const response = await fetch(`${BACKEND_URL}/auth/login`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, password }),
+ })
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ message: 'Login failed' }))
+ throw new Error(error.message || 'Login failed')
+ }
+
+ const data = await response.json()
+ const { access_token, user: userData } = data
+
+ setToken(access_token)
+ setUser(userData)
+ localStorage.setItem('auth_token', access_token)
+ localStorage.setItem('auth_user', JSON.stringify(userData))
+ }
+
+ const register = async (email: string, password: string, firstName: string, lastName: string) => {
+ const response = await fetch(`${BACKEND_URL}/auth/register`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, password, firstName, lastName }),
+ })
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ message: 'Registration failed' }))
+ throw new Error(error.message || 'Registration failed')
+ }
+
+ const data = await response.json()
+ const { access_token, user: userData } = data
+
+ setToken(access_token)
+ setUser(userData)
+ localStorage.setItem('auth_token', access_token)
+ localStorage.setItem('auth_user', JSON.stringify(userData))
+ }
+
+ const logout = () => {
+ setToken(null)
+ setUser(null)
+ localStorage.removeItem('auth_token')
+ localStorage.removeItem('auth_user')
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export function useAuth() {
+ const context = useContext(AuthContext)
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider')
+ }
+ return context
+}
diff --git a/src/graphql/client.ts b/src/graphql/client.ts
index 2fd4ab8..2fd1dda 100644
--- a/src/graphql/client.ts
+++ b/src/graphql/client.ts
@@ -1,10 +1,24 @@
-import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
+import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client'
+import { setContext } from '@apollo/client/link/context'
const httpLink = new HttpLink({
- uri: import.meta.env.VITE_GRAPHQL_URL || 'http://localhost:4000/graphql',
+ uri: import.meta.env.VITE_GRAPHQL_URL || 'http://localhost:3000/graphql',
+})
+
+const authLink = setContext((_, { headers }) => {
+ // Get the authentication token from localStorage
+ const token = localStorage.getItem('auth_token')
+
+ // Return the headers to the context so httpLink can read them
+ return {
+ headers: {
+ ...headers,
+ authorization: token ? `Bearer ${token}` : '',
+ },
+ }
})
export const client = new ApolloClient({
- link: httpLink,
+ link: ApolloLink.from([authLink, httpLink]),
cache: new InMemoryCache(),
})
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts
index 5413123..f69745f 100644
--- a/src/graphql/queries.ts
+++ b/src/graphql/queries.ts
@@ -82,3 +82,21 @@ export const GET_CHECKOUT = gql`
}
}
`
+
+export const MY_ORDERS = gql`
+ query MyOrders {
+ myOrders {
+ id
+ status
+ total
+ items {
+ productId
+ name
+ quantity
+ price
+ }
+ paymentMethod
+ createdAt
+ }
+ }
+`