diff --git a/webui/public/file.svg b/webui/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/webui/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webui/public/globe.svg b/webui/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/webui/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webui/public/logo.png b/webui/public/logo.png new file mode 100644 index 0000000..b0ae2d4 Binary files /dev/null and b/webui/public/logo.png differ diff --git a/webui/public/next.svg b/webui/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/webui/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webui/public/vercel.svg b/webui/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/webui/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webui/public/window.svg b/webui/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/webui/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webui/src/app/components/ImprinkAppBar.js b/webui/src/app/components/ImprinkAppBar.js index 5ab6649..d635d44 100644 --- a/webui/src/app/components/ImprinkAppBar.js +++ b/webui/src/app/components/ImprinkAppBar.js @@ -15,7 +15,7 @@ import { useTheme, Paper } from "@mui/material"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useUser } from "@auth0/nextjs-auth0"; import { Menu as MenuIcon, @@ -29,43 +29,16 @@ import { BugReport } from "@mui/icons-material"; import ThemeToggleButton from "@/app/components/theme/ThemeToggleButton"; -import clientApi from "@/lib/clientApi"; +import useRoles from "@/app/components/hooks/useRoles"; export default function ImprinkAppBar() { const { user, error, isLoading } = useUser(); + const { isMerchant, isAdmin } = useRoles(); const [anchorEl, setAnchorEl] = useState(null); - const [userRoles, setUserRoles] = useState([]); - const [rolesLoading, setRolesLoading] = useState(false); const theme = useTheme(); const { isDarkMode, toggleTheme } = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); - useEffect(() => { - const fetchUserRoles = async () => { - if (!user) { - setUserRoles([]); - return; - } - - setRolesLoading(true); - try { - const response = await clientApi.get('/users/me/roles'); - const roles = response.data.map(role => role.roleName.toLowerCase()); - setUserRoles(roles); - } catch (error) { - console.error('Failed to fetch user roles:', error); - setUserRoles([]); - } finally { - setRolesLoading(false); - } - }; - - fetchUserRoles(); - }, [user]); - - const isMerchant = userRoles.includes('merchant') || userRoles.includes('admin'); - const isAdmin = userRoles.includes('admin'); - const handleMenuOpen = (event) => { setAnchorEl(event.currentTarget); }; @@ -74,7 +47,6 @@ export default function ImprinkAppBar() { setAnchorEl(null); }; - // Regular navigation links (excluding admin-specific ones) const navigationLinks = [ { label: 'Home', href: '/', icon: , show: true }, { label: 'Gallery', href: '/gallery', icon: , show: true }, @@ -82,7 +54,6 @@ export default function ImprinkAppBar() { { label: 'Merchant', href: '/merchant', icon: , show: isMerchant }, ]; - // Admin-specific links for the separate bar const adminLinks = [ { label: 'Dashboard', href: '/dashboard', icon: , show: isMerchant }, { label: 'Admin', href: '/admin', icon: , show: isAdmin }, @@ -206,7 +177,6 @@ export default function ImprinkAppBar() { > - {/* All navigation links - regular and admin mixed together */} {[...visibleLinks, ...visibleAdminLinks].map((link) => ( - {/* Admin Bar - appears below main app bar */} {renderAdminBar()} ) diff --git a/webui/src/app/components/hooks/useRoles.js b/webui/src/app/components/hooks/useRoles.js new file mode 100644 index 0000000..c68a391 --- /dev/null +++ b/webui/src/app/components/hooks/useRoles.js @@ -0,0 +1,68 @@ +import { useState, useEffect } from 'react'; +import { useUser } from '@auth0/nextjs-auth0'; +import clientApi from '@/lib/clientApi'; + +export const useRoles = () => { + const { user } = useUser(); + const [roles, setRoles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchUserRoles = async () => { + if (!user) { + setRoles([]); + setError(null); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await clientApi.get('/users/me/roles'); + const userRoles = response.data.map(role => role.roleName.toLowerCase()); + setRoles(userRoles); + } catch (err) { + console.error('Failed to fetch user roles:', err); + setError(err); + setRoles([]); + } finally { + setIsLoading(false); + } + }; + + fetchUserRoles().then(r => console.log(r)); + }, [user]); + + const hasRole = (roleName) => { + return roles.includes(roleName.toLowerCase()); + }; + + const hasAnyRole = (roleNames) => { + return roleNames.some(roleName => hasRole(roleName)); + }; + + const hasAllRoles = (roleNames) => { + return roleNames.every(roleName => hasRole(roleName)); + }; + + // Common role checks based on your existing logic + const isMerchant = hasAnyRole(['merchant', 'admin']); + const isAdmin = hasRole('admin'); + const isCustomer = hasRole('customer'); + + return { + roles, + isLoading, + error, + hasRole, + hasAnyRole, + hasAllRoles, + isMerchant, + isAdmin, + isCustomer + }; +}; + +export default useRoles; \ No newline at end of file diff --git a/webui/src/app/gallery/page.js b/webui/src/app/gallery/page.js new file mode 100644 index 0000000..ff58257 --- /dev/null +++ b/webui/src/app/gallery/page.js @@ -0,0 +1,661 @@ +'use client'; + +import { + Box, + Container, + Typography, + Button, + Card, + CardContent, + CardMedia, + Grid, + Chip, + CircularProgress, + Alert, + TextField, + InputAdornment, + Pagination, + FormControl, + Select, + MenuItem, + InputLabel, + Drawer, + List, + ListItem, + ListItemButton, + ListItemText, + Collapse, + IconButton, + useMediaQuery, + useTheme, + Paper, + Slider, + Switch, + FormControlLabel +} from '@mui/material'; +import { useState, useEffect, useCallback } from 'react'; +import { + Search, + ExpandLess, + ExpandMore, + Close as CloseIcon, + Tune as TuneIcon +} from '@mui/icons-material'; +import clientApi from "@/lib/clientApi"; +import useRoles from "@/app/components/hooks/useRoles"; + +const SORT_OPTIONS = [ + { value: 'Name-ASC', label: 'Name (A-Z)' }, + { value: 'Name-DESC', label: 'Name (Z-A)' }, + { value: 'Price-ASC', label: 'Price (Low to High)' }, + { value: 'Price-DESC', label: 'Price (High to Low)' }, + { value: 'CreatedDate-DESC', label: 'Newest First' }, + { value: 'CreatedDate-ASC', label: 'Oldest First' } +]; + +const PAGE_SIZE_OPTIONS = [12, 24, 48, 96]; + +export default function GalleryPage() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const { isAdmin } = useRoles(); + + const heightOffset = isAdmin ? 192 : 128; + + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [totalPages, setTotalPages] = useState(0); + const [totalCount, setTotalCount] = useState(0); + + const [categories, setCategories] = useState([]); + const [categoriesLoading, setCategoriesLoading] = useState(true); + const [expandedCategories, setExpandedCategories] = useState(new Set()); + + const [filters, setFilters] = useState({ + pageNumber: 1, + pageSize: 24, + searchTerm: '', + categoryId: '', + minPrice: 0, + maxPrice: 1000, + isActive: true, + isCustomizable: null, + sortBy: 'Name', + sortDirection: 'ASC' + }); + + const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); + const [priceRange, setPriceRange] = useState([0, 1000]); + const [searchInput, setSearchInput] = useState(''); + + useEffect(() => { + const fetchCategories = async () => { + try { + const response = await clientApi.get('/products/categories'); + setCategories(response.data); + } catch (err) { + console.error('Error fetching categories:', err); + } finally { + setCategoriesLoading(false); + } + }; + + fetchCategories(); + }, []); + + const fetchProducts = useCallback(async () => { + setLoading(true); + try { + const params = { + PageNumber: filters.pageNumber, + PageSize: filters.pageSize, + IsActive: filters.isActive, + SortBy: filters.sortBy, + SortDirection: filters.sortDirection + }; + + if (filters.searchTerm) params.SearchTerm = filters.searchTerm; + if (filters.categoryId) params.CategoryId = filters.categoryId; + if (filters.minPrice > 0) params.MinPrice = filters.minPrice; + if (filters.maxPrice < 1000) params.MaxPrice = filters.maxPrice; + if (filters.isCustomizable !== null) params.IsCustomizable = filters.isCustomizable; + + const response = await clientApi.get('/products/', { params }); + + setProducts(response.data.items); + setTotalPages(response.data.totalPages); + setTotalCount(response.data.totalCount); + } catch (err) { + setError('Failed to load products'); + console.error('Error fetching products:', err); + } finally { + setLoading(false); + } + }, [filters]); + + useEffect(() => { + fetchProducts(); + }, [fetchProducts]); + + const handleFilterChange = (key, value) => { + setFilters(prev => ({ + ...prev, + [key]: value, + pageNumber: key !== 'pageNumber' ? 1 : value + })); + }; + + const handleSearch = () => { + handleFilterChange('searchTerm', searchInput); + }; + + const handlePriceRangeChange = (event, newValue) => { + setPriceRange(newValue); + }; + + const handlePriceRangeCommitted = (event, newValue) => { + handleFilterChange('minPrice', newValue[0]); + handleFilterChange('maxPrice', newValue[1]); + }; + + const handleSortChange = (value) => { + const [sortBy, sortDirection] = value.split('-'); + handleFilterChange('sortBy', sortBy); + handleFilterChange('sortDirection', sortDirection); + }; + + const toggleCategoryExpansion = (categoryId) => { + const newExpanded = new Set(expandedCategories); + if (newExpanded.has(categoryId)) { + newExpanded.delete(categoryId); + } else { + newExpanded.add(categoryId); + } + setExpandedCategories(newExpanded); + }; + + const getChildCategories = (parentId) => { + return categories.filter(cat => cat.parentCategoryId === parentId); + }; + + const getParentCategories = () => { + return categories.filter(cat => !cat.parentCategoryId); + }; + + const CategorySidebar = () => ( + + + Categories + + + + + + handleFilterChange('categoryId', '')} + sx={{ borderRadius: 1, mb: 0.5 }} + > + + + + + {getParentCategories().map((category) => { + const childCategories = getChildCategories(category.id); + const hasChildren = childCategories.length > 0; + const isExpanded = expandedCategories.has(category.id); + + return ( + + + handleFilterChange('categoryId', category.id)} + sx={{ borderRadius: 1, mb: 0.5 }} + > + + {hasChildren && ( + { + e.stopPropagation(); + toggleCategoryExpansion(category.id); + }} + > + {isExpanded ? : } + + )} + + + + {hasChildren && ( + + + {childCategories.map((childCategory) => ( + + handleFilterChange('categoryId', childCategory.id)} + sx={{ borderRadius: 1, mb: 0.5 }} + > + + + + ))} + + + )} + + ); + })} + + + + + + Price Range + + + `$${value}`} + /> + + ${priceRange[0]} + ${priceRange[1]} + + + + handleFilterChange('isCustomizable', e.target.checked ? true : null)} + /> + } + label="Customizable Only" + sx={{ mb: 0 }} + /> + + + ); + + return ( + + + {!isMobile && ( + + + {categoriesLoading ? ( + + + + ) : ( + + )} + + + )} + + + + + + Product Gallery + + + Explore our complete collection of customizable products + + + + + + {isMobile && ( + + setMobileDrawerOpen(true)} + sx={{ mr: 1 }} + size="small" + > + + + + )} + + + setSearchInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSearch()} + size={isMobile ? "small" : "medium"} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchInput && ( + + + + + + ) + }} + /> + + + + + Sort By + + + + + + + Per Page + + + + + + + Showing {products.length} of {totalCount} products + + + + + + {loading && ( + + + + )} + + {error && ( + + {error} + + )} + + {!loading && !error && products.length === 0 && ( + + + No products found + + + Try adjusting your search criteria or filters + + + )} + + {!loading && !error && products.length > 0 && ( + + {products.map((product) => ( + + + + + {product.name} + + + {product.category && ( + + )} + + + {product.description} + + + + + From ${product.basePrice?.toFixed(2)} + + {product.isCustomizable && ( + + )} + + + + + + ))} + + )} + + {!loading && !error && totalPages > 1 && ( + + handleFilterChange('pageNumber', page)} + color="primary" + size={isMobile ? 'small' : 'large'} + showFirstButton={!isMobile} + showLastButton={!isMobile} + /> + + )} + + + + + setMobileDrawerOpen(false)} + ModalProps={{ keepMounted: true }} + PaperProps={{ + sx: { + width: 280, + display: 'flex', + flexDirection: 'column' + } + }} + > + + + Filters + + setMobileDrawerOpen(false)}> + + + + {categoriesLoading ? ( + + + + ) : ( + + )} + + + ); +} \ No newline at end of file diff --git a/webui/src/app/page.js b/webui/src/app/page.js index 4f50d30..3c9238b 100644 --- a/webui/src/app/page.js +++ b/webui/src/app/page.js @@ -157,6 +157,7 @@ export default function HomePage() {