-
+
{backLink && }
{children}
diff --git a/src/components/layout/header/AuthSection.tsx b/src/components/layout/header/AuthSection.tsx
index 518bf16..a9bcbb4 100644
--- a/src/components/layout/header/AuthSection.tsx
+++ b/src/components/layout/header/AuthSection.tsx
@@ -1,14 +1,26 @@
import { Link } from 'react-router-dom'
import { useAuth } from '#src/hooks/useAuth'
import { useCart } from '#src/hooks/useCart'
+import { usePermissions } from '#src/hooks/usePermissions'
export function AuthSection() {
const { isAuthenticated, user, logout } = useAuth()
const { isCartOpen, toggleCart } = useCart()
+ const { hasAdminAccess } = usePermissions()
+
+ const canViewAdmin = hasAdminAccess()
if (isAuthenticated) {
return (
<>
+ {canViewAdmin && (
+
+ Admin
+
+ )}
{user?.email}
diff --git a/src/components/layout/header/Header.tsx b/src/components/layout/header/Header.tsx
index 36649fa..b0333e9 100644
--- a/src/components/layout/header/Header.tsx
+++ b/src/components/layout/header/Header.tsx
@@ -13,4 +13,3 @@ export function Header() {
)
}
-
diff --git a/src/contexts/auth/AuthContext.types.ts b/src/contexts/auth/AuthContext.types.ts
index df65556..adc122f 100644
--- a/src/contexts/auth/AuthContext.types.ts
+++ b/src/contexts/auth/AuthContext.types.ts
@@ -1,8 +1,18 @@
+export type UserPermission =
+ | 'admin:read'
+ | 'products:read'
+ | 'products:write'
+ | 'orders:read'
+ | 'orders:write'
+ | 'permissions:read'
+ | 'permissions:write'
+
export interface User {
id: string
email: string
firstName: string
lastName: string
+ permissions?: UserPermission[]
}
export interface AuthContextType {
diff --git a/src/data/products.ts b/src/data/products.ts
index 5ed3404..74c21a2 100644
--- a/src/data/products.ts
+++ b/src/data/products.ts
@@ -7,6 +7,5 @@ export type Product = {
description: string
badge?: string
featured?: boolean
- rating: number
colors: string[]
}
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts
index 8a2b609..abffaf7 100644
--- a/src/graphql/queries.ts
+++ b/src/graphql/queries.ts
@@ -114,6 +114,19 @@ export const GET_REVIEWS = gql`
}
`
+export const GET_REVIEWS_BY_PRODUCT_IDS = gql`
+ query GetReviewsByProductIds($productIds: [ID!]!) {
+ reviewsByProductIds(productIds: $productIds) {
+ id
+ productId
+ text
+ rating
+ userName
+ createdAt
+ }
+ }
+`
+
export const CREATE_REVIEW = gql`
mutation CreateReview($createReviewInput: CreateReviewInput!) {
createReview(createReviewInput: $createReviewInput) {
@@ -132,3 +145,104 @@ export const DELETE_REVIEW = gql`
deleteReview(id: $id)
}
`
+
+export const GET_ADMIN_PRODUCTS = gql`
+ query AdminProducts($category: String) {
+ adminProducts(category: $category) {
+ id
+ name
+ category
+ price
+ image
+ description
+ badge
+ featured
+ colors
+ createdAt
+ updatedAt
+ }
+ }
+`
+
+export const CREATE_PRODUCT = gql`
+ mutation CreateProduct($createProductInput: CreateProductInput!) {
+ createProduct(createProductInput: $createProductInput) {
+ id
+ name
+ category
+ price
+ image
+ description
+ badge
+ featured
+ colors
+ createdAt
+ updatedAt
+ }
+ }
+`
+
+export const UPDATE_PRODUCT = gql`
+ mutation UpdateProduct($id: ID!, $updateProductInput: CreateProductInput!) {
+ updateProduct(id: $id, updateProductInput: $updateProductInput) {
+ id
+ name
+ category
+ price
+ image
+ description
+ badge
+ featured
+ colors
+ createdAt
+ updatedAt
+ }
+ }
+`
+
+export const DELETE_PRODUCT = gql`
+ mutation DeleteProduct($id: ID!) {
+ removeProduct(id: $id)
+ }
+`
+
+export const GET_ADMIN_CHECKOUTS = gql`
+ query AdminCheckouts($status: CheckoutStatus) {
+ adminCheckouts(status: $status) {
+ id
+ status
+ total
+ subtotal
+ tax
+ shipping
+ items {
+ productId
+ name
+ quantity
+ price
+ }
+ shippingAddress {
+ firstName
+ lastName
+ address
+ city
+ state
+ zipCode
+ country
+ }
+ paymentMethod
+ createdAt
+ updatedAt
+ }
+ }
+`
+
+export const UPDATE_CHECKOUT_STATUS = gql`
+ mutation UpdateCheckoutStatus($id: ID!, $status: CheckoutStatus!) {
+ updateCheckoutStatus(id: $id, status: $status) {
+ id
+ status
+ updatedAt
+ }
+ }
+`
diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts
new file mode 100644
index 0000000..290a765
--- /dev/null
+++ b/src/hooks/usePermissions.ts
@@ -0,0 +1,26 @@
+import { useMemo } from 'react'
+import { useAuth } from './useAuth'
+import type { UserPermission } from '../contexts/auth/AuthContext.types'
+import {
+ hasPermission,
+ hasAnyPermission,
+ hasAllPermissions,
+ hasAdminAccess,
+} from '../utils/permissions'
+
+export function usePermissions() {
+ const { user } = useAuth()
+
+ return useMemo(() => {
+ const permissions = user?.permissions || []
+ return {
+ permissions,
+ hasPermission: (permission: UserPermission) => hasPermission(permissions, permission),
+ hasAnyPermission: (permissionList: UserPermission[]) =>
+ hasAnyPermission(permissions, permissionList),
+ hasAllPermissions: (permissionList: UserPermission[]) =>
+ hasAllPermissions(permissions, permissionList),
+ hasAdminAccess: () => hasAdminAccess(permissions),
+ }
+ }, [user?.permissions])
+}
diff --git a/src/pages/AdminDashboardPage.tsx b/src/pages/AdminDashboardPage.tsx
new file mode 100644
index 0000000..558e9a3
--- /dev/null
+++ b/src/pages/AdminDashboardPage.tsx
@@ -0,0 +1,53 @@
+import { Link } from 'react-router-dom'
+import { usePermissions } from '#src/hooks/usePermissions'
+
+export default function AdminDashboardPage() {
+ const { hasPermission } = usePermissions()
+
+ const canManageProducts = hasPermission('products:read') || hasPermission('products:write')
+ const canManageOrders = hasPermission('orders:read') || hasPermission('orders:write')
+
+ return (
+
+
Admin Dashboard
+
+
+ {canManageProducts && (
+
+
Products
+
+ Manage products, create new items, and update existing ones.
+
+
Manage Products →
+
+ )}
+
+ {canManageOrders && (
+
+
Orders
+
+ View and manage orders, update statuses, and track fulfillment.
+
+
Manage Orders →
+
+ )}
+
+
+ {!canManageProducts && !canManageOrders && (
+
+
+ You don't have access to any admin features. Contact an administrator to request
+ permissions.
+
+
+ )}
+
+ )
+}
+
diff --git a/src/pages/AdminOrdersPage.tsx b/src/pages/AdminOrdersPage.tsx
new file mode 100644
index 0000000..903cd5f
--- /dev/null
+++ b/src/pages/AdminOrdersPage.tsx
@@ -0,0 +1,180 @@
+import { useState } from 'react'
+import { useQuery, useMutation } from '@apollo/client/react'
+import { GET_ADMIN_CHECKOUTS, UPDATE_CHECKOUT_STATUS } from '#src/graphql/queries'
+import { LoadingState } from '#src/components/LoadingState'
+import { ErrorMessage } from '#src/components/ErrorMessage'
+import { usePermissions } from '#src/hooks/usePermissions'
+import { currency } from '#src/utils/constants'
+
+type CheckoutStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'
+
+type CheckoutItem = {
+ productId: string
+ name: string
+ quantity: number
+ price: number
+}
+
+type Address = {
+ firstName: string
+ lastName: string
+ address: string
+ city: string
+ state: string
+ zipCode: string
+ country: string
+}
+
+type Checkout = {
+ id: string
+ status: CheckoutStatus
+ total: number
+ subtotal: number
+ tax: number
+ shipping: number
+ items: CheckoutItem[]
+ shippingAddress: Address
+ paymentMethod: string
+ createdAt: string
+ updatedAt: string
+}
+
+type CheckoutsQueryResult = {
+ adminCheckouts: Checkout[]
+}
+
+export default function AdminOrdersPage() {
+ const { hasPermission } = usePermissions()
+ const [statusFilter, setStatusFilter] = useState
(null)
+ const { loading, error, data, refetch } = useQuery(GET_ADMIN_CHECKOUTS, {
+ variables: statusFilter ? { status: statusFilter } : {},
+ })
+ const [updateStatus] = useMutation(UPDATE_CHECKOUT_STATUS, {
+ onCompleted: () => refetch(),
+ })
+
+ const canWrite = hasPermission('orders:write')
+
+ if (loading) return
+ if (error) return
+
+ const checkouts = data?.adminCheckouts || []
+
+ const handleStatusChange = async (id: string, newStatus: CheckoutStatus) => {
+ try {
+ await updateStatus({
+ variables: { id, status: newStatus },
+ })
+ } catch {
+ alert('Failed to update order status')
+ }
+ }
+
+ const statuses: CheckoutStatus[] = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'CANCELLED']
+
+ return (
+
+
+
Admin Orders
+
+
+
+
+
+
+ {checkouts.map((checkout) => (
+
+
+
+
Order #{checkout.id.slice(0, 8)}
+
+ {checkout.shippingAddress.firstName} {checkout.shippingAddress.lastName}
+
+
+ {new Date(checkout.createdAt).toLocaleDateString()}
+
+
+
+
{currency.format(checkout.total)}
+
+
+ {checkout.status}
+
+ {canWrite && (
+
+ )}
+
+
+
+
+
+
+
+
Items:
+
+ {checkout.items.map((item, idx) => (
+ -
+ {item.name} × {item.quantity} -{' '}
+ {currency.format(item.price * item.quantity)}
+
+ ))}
+
+
+
+
Shipping Address:
+
+ {checkout.shippingAddress.address}
+
+ {checkout.shippingAddress.city}, {checkout.shippingAddress.state}{' '}
+ {checkout.shippingAddress.zipCode}
+
+ {checkout.shippingAddress.country}
+
+
+
+
+
+ ))}
+
+ {checkouts.length === 0 && (
+
+ No orders found
+
+ )}
+
+
+ )
+}
diff --git a/src/pages/AdminProductsPage.tsx b/src/pages/AdminProductsPage.tsx
new file mode 100644
index 0000000..47e8535
--- /dev/null
+++ b/src/pages/AdminProductsPage.tsx
@@ -0,0 +1,139 @@
+import { useState } from 'react'
+import { useQuery, useMutation } from '@apollo/client/react'
+import { GET_ADMIN_PRODUCTS, DELETE_PRODUCT } from '#src/graphql/queries'
+import { LoadingState } from '#src/components/LoadingState'
+import { ErrorMessage } from '#src/components/ErrorMessage'
+import { usePermissions } from '#src/hooks/usePermissions'
+import { currency } from '#src/utils/constants'
+import { Link } from 'react-router-dom'
+import ProductForm from '#src/components/admin/ProductForm'
+
+type Product = {
+ id: string
+ name: string
+ category: string
+ price: number
+ image: string
+ description: string
+ badge?: string
+ featured: boolean
+ colors: string[]
+ createdAt: string
+ updatedAt: string
+}
+
+type ProductsQueryResult = {
+ adminProducts: Product[]
+}
+
+export default function AdminProductsPage() {
+ const { hasPermission } = usePermissions()
+ const [editingProduct, setEditingProduct] = useState(null)
+ const [showCreateForm, setShowCreateForm] = useState(false)
+ const { loading, error, data, refetch } = useQuery(GET_ADMIN_PRODUCTS)
+ const [deleteProduct] = useMutation(DELETE_PRODUCT, {
+ onCompleted: () => refetch(),
+ })
+
+ const canWrite = hasPermission('products:write')
+
+ if (loading) return
+ if (error) return
+
+ const products = data?.adminProducts || []
+
+ const handleDelete = async (id: string) => {
+ if (!confirm('Are you sure you want to delete this product?')) return
+ try {
+ await deleteProduct({ variables: { id } })
+ } catch {
+ alert('Failed to delete product')
+ }
+ }
+
+ if (showCreateForm || editingProduct) {
+ return (
+ {
+ setEditingProduct(null)
+ setShowCreateForm(false)
+ }}
+ onSuccess={() => {
+ setEditingProduct(null)
+ setShowCreateForm(false)
+ refetch()
+ }}
+ />
+ )
+ }
+
+ return (
+
+
+
Admin Products
+ {canWrite && (
+
+ )}
+
+
+
+
+
+
+ | Name |
+ Category |
+ Price |
+ Featured |
+ Actions |
+
+
+
+ {products.map((product) => (
+
+ | {product.name} |
+ {product.category} |
+ {currency.format(product.price)} |
+ {product.featured ? 'Yes' : 'No'} |
+
+
+ {canWrite && (
+ <>
+
+
+ >
+ )}
+
+ View
+
+
+ |
+
+ ))}
+
+
+ {products.length === 0 && (
+
No products found
+ )}
+
+
+ )
+}
diff --git a/src/routes.tsx b/src/routes.tsx
index e3bb05d..b4c0866 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,4 +1,6 @@
+import { lazy } from 'react'
import { ProtectedRoute } from '#src/components/ProtectedRoute'
+import { AdminProtectedRoute } from '#src/components/AdminProtectedRoute'
import { HomePage } from '#src/pages/HomePage'
import ProductsPage from '#src/ProductsPage'
import ProductPage from '#src/ProductPage'
@@ -8,6 +10,17 @@ import LoginPage from '#src/LoginPage'
import RegisterPage from '#src/RegisterPage'
import UserOrdersPage from '#src/UserOrdersPage'
+// Lazy load admin pages
+const AdminDashboardPage = lazy(() =>
+ import('#src/pages/AdminDashboardPage').then((m) => ({ default: m.default })),
+)
+const AdminProductsPage = lazy(() =>
+ import('#src/pages/AdminProductsPage').then((m) => ({ default: m.default })),
+)
+const AdminOrdersPage = lazy(() =>
+ import('#src/pages/AdminOrdersPage').then((m) => ({ default: m.default })),
+)
+
export const routes = [
{
path: '/',
@@ -49,4 +62,31 @@ export const routes = [
),
backLink: { to: '/products', label: '← Back to products' },
},
+ {
+ path: '/admin',
+ element: (
+
+
+
+ ),
+ backLink: { to: '/', label: '← Back to main site' },
+ },
+ {
+ path: '/admin/products',
+ element: (
+
+
+
+ ),
+ backLink: { to: '/admin', label: '← Back to admin' },
+ },
+ {
+ path: '/admin/orders',
+ element: (
+
+
+
+ ),
+ backLink: { to: '/admin', label: '← Back to admin' },
+ },
]
diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts
new file mode 100644
index 0000000..21e7029
--- /dev/null
+++ b/src/utils/permissions.ts
@@ -0,0 +1,42 @@
+import type { UserPermission } from '../contexts/auth/AuthContext.types'
+
+/**
+ * Check if user has a specific permission
+ */
+export function hasPermission(
+ userPermissions: UserPermission[] | undefined,
+ permission: UserPermission,
+): boolean {
+ if (!userPermissions) return false
+ return userPermissions.includes(permission)
+}
+
+/**
+ * Check if user has any of the specified permissions
+ */
+export function hasAnyPermission(
+ userPermissions: UserPermission[] | undefined,
+ permissions: UserPermission[],
+): boolean {
+ if (!userPermissions) return false
+ return permissions.some((permission) => userPermissions.includes(permission))
+}
+
+/**
+ * Check if user has all of the specified permissions
+ */
+export function hasAllPermissions(
+ userPermissions: UserPermission[] | undefined,
+ permissions: UserPermission[],
+): boolean {
+ if (!userPermissions) return false
+ return permissions.every((permission) => userPermissions.includes(permission))
+}
+
+/**
+ * Check if user has admin access (requires admin:read permission)
+ */
+export function hasAdminAccess(userPermissions: UserPermission[] | undefined): boolean {
+ if (!userPermissions) return false
+ return hasPermission(userPermissions, 'admin:read')
+}
diff --git a/test/hooks/useCart.test.tsx b/test/hooks/useCart.test.tsx
index af7ddf8..cc9fabc 100644
--- a/test/hooks/useCart.test.tsx
+++ b/test/hooks/useCart.test.tsx
@@ -15,7 +15,6 @@ const mockProducts: Product[] = [
badge: undefined,
featured: false,
colors: ['Red'],
- rating: 4.5,
},
]