diff --git a/src/Imprink.Application/Imprink.Application.csproj b/src/Imprink.Application/Imprink.Application.csproj index 17480c4..bde7f6a 100644 --- a/src/Imprink.Application/Imprink.Application.csproj +++ b/src/Imprink.Application/Imprink.Application.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/src/Imprink.WebApi/Controllers/SeedingController.cs b/src/Imprink.WebApi/Controllers/SeedingController.cs index 009b765..e723b3a 100644 --- a/src/Imprink.WebApi/Controllers/SeedingController.cs +++ b/src/Imprink.WebApi/Controllers/SeedingController.cs @@ -8,7 +8,6 @@ namespace Imprink.WebApi.Controllers; public class SeedingController(Seeder seeder) : ControllerBase { [HttpGet] - [Authorize(Roles = "Admin")] public async Task> Seed() { await seeder.SeedAsync(); diff --git a/src/Imprink.WebApi/Seeder.cs b/src/Imprink.WebApi/Seeder.cs index 45f84f0..142a462 100644 --- a/src/Imprink.WebApi/Seeder.cs +++ b/src/Imprink.WebApi/Seeder.cs @@ -19,7 +19,6 @@ public class Seeder(ApplicationDbContext context) private readonly string[] _textileImages = [ "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=500", - "https://images.unsplash.com/photo-1583743814966-8936f37f4ad2?w=500", "https://images.unsplash.com/photo-1571945153237-4929e783af4a?w=500", "https://images.unsplash.com/photo-1618354691373-d851c5c3a990?w=500", "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=500" @@ -29,14 +28,11 @@ public class Seeder(ApplicationDbContext context) [ "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=500", "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=500", - "https://images.unsplash.com/photo-1544966503-7cc5ac882d2e?w=500", "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=500" ]; private readonly string[] _paperImages = [ - "https://images.unsplash.com/photo-1586281010691-79ab3d0f2102?w=500", - "https://images.unsplash.com/photo-1594736797933-d0401ba2fe65?w=500", "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500", "https://images.unsplash.com/photo-1584464491033-06628f3a6b7b?w=500" ]; diff --git a/webui/src/app/components/PaymentForm.js b/webui/auth0-templates/PaymentForm.js similarity index 100% rename from webui/src/app/components/PaymentForm.js rename to webui/auth0-templates/PaymentForm.js diff --git a/webui/auth0-templates/page.js b/webui/auth0-templates/page.js new file mode 100644 index 0000000..e14237a --- /dev/null +++ b/webui/auth0-templates/page.js @@ -0,0 +1,143 @@ +'use client'; + +import { useState } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements } from '@stripe/react-stripe-js'; +import PaymentForm from './components/PaymentForm'; +import './globals.css'; + +const stripePromise = loadStripe(''); + +const products = [ + { id: '1', name: 'Premium Widget', price: 2999, description: 'High-quality widget for professionals' }, + { id: '2', name: 'Standard Widget', price: 1999, description: 'Reliable widget for everyday use' }, + { id: '3', name: 'Basic Widget', price: 999, description: 'Entry-level widget for beginners' } +]; + +export default function Home() { + const [selectedProduct, setSelectedProduct] = useState(null); + const [clientSecret, setClientSecret] = useState(''); + const [orderId, setOrderId] = useState(''); + const [loading, setLoading] = useState(false); + + const handleProductSelect = async (product) => { + setLoading(true); + setSelectedProduct(product); + + const newOrderId = Math.floor(Math.random() * 10000).toString(); + setOrderId(newOrderId); + + try { + const response = await fetch('https://impr.ink/api/stripe/create-payment-intent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + amount: product.price, + orderId: newOrderId + }), + }); + + const data = await response.json(); + + if (data.clientSecret) { + setClientSecret(data.clientSecret); + } else { + console.error('Error creating payment intent:', data.error); + } + } catch (error) { + console.error('Error:', error); + } finally { + setLoading(false); + } + }; + + const handlePaymentSuccess = () => { + setSelectedProduct(null); + setClientSecret(''); + setOrderId(''); + }; + + const handleBackToProducts = () => { + setSelectedProduct(null); + setClientSecret(''); + setOrderId(''); + }; + + const appearance = { + theme: 'stripe', + variables: { + colorPrimary: '#0570de', + colorBackground: '#ffffff', + colorText: '#30313d', + colorDanger: '#df1b41', + fontFamily: 'Ideal Sans, system-ui, sans-serif', + spacingUnit: '2px', + borderRadius: '4px', + }, + }; + + const options = { + clientSecret, + appearance, + }; + + return ( +
+
+

🛍️ Stripe Payment Demo

+

Select a product to purchase

+
+ + {!selectedProduct ? ( +
+

Products

+
+ {products.map((product) => ( +
+

{product.name}

+

{product.description}

+

${(product.price / 100).toFixed(2)}

+ +
+ ))} +
+
+ ) : ( +
+
+

Order Summary

+
+

Product: {selectedProduct.name}

+

Order ID: {orderId}

+

Amount: ${(selectedProduct.price / 100).toFixed(2)}

+
+
+ + {clientSecret && ( + + + + )} + + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/webui/next.config.mjs b/webui/next.config.mjs index 0b5a886..7859408 100644 --- a/webui/next.config.mjs +++ b/webui/next.config.mjs @@ -1,14 +1,5 @@ /** @type {import('next').NextConfig} */ -const nextConfig = { - webpack: (config, { dev }) => { - if (dev) { - config.watchOptions = { - poll: 1000, - aggregateTimeout: 300, - } - } - return config - }, -} +const nextConfig = {} + +export default nextConfig; -export default nextConfig; \ No newline at end of file diff --git a/webui/package-lock.json b/webui/package-lock.json index dfadc04..2270b2a 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -9,19 +9,25 @@ "version": "0.1.0", "dependencies": { "@auth0/nextjs-auth0": "^4.6.1", + "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", + "@emotion/serialize": "^1.3.3", "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.1", + "@mui/material-nextjs": "^7.1.1", "@mui/x-data-grid": "^8.5.2", "@mui/x-date-pickers": "^8.5.2", "@stripe/react-stripe-js": "^3.7.0", "@stripe/stripe-js": "^7.3.1", "axios": "^1.9.0", + "i18next": "^25.2.1", "lucide-react": "^0.516.0", "next": "15.3.3", + "next-i18next": "^15.4.2", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-i18next": "^15.5.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -910,6 +916,41 @@ } } }, + "node_modules/@mui/material-nextjs": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-7.1.1.tgz", + "integrity": "sha512-6/tjmViYMI7XIqDTqK+n4t5B07YfVDq72emdBy/o8FLHsV7u477Ro0Aago2MQu8FrBQWDvzvvRkynIb02GjDBQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.4", + "@emotion/server": "^11.11.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "next": "^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/cache": { + "optional": true + }, + "@emotion/server": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/private-theming": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz", @@ -1650,6 +1691,16 @@ "tailwindcss": "4.1.8" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1667,7 +1718,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.7.tgz", "integrity": "sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1854,6 +1904,17 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/core-js": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", + "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -2201,6 +2262,52 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz", + "integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.0.tgz", + "integrity": "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2705,6 +2812,42 @@ } } }, + "node_modules/next-i18next": { + "version": "15.4.2", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.4.2.tgz", + "integrity": "sha512-zgRxWf7kdXtM686ecGIBQL+Bq0+DqAhRlasRZ3vVF0TmrNTWkVhs52n//oU3Fj5O7r/xOKkECDUwfOuXVwTK/g==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@types/hoist-non-react-statics": "^3.3.6", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.6.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": ">= 23.7.13", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": ">= 13.5.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -2875,6 +3018,32 @@ "react": "^19.1.0" } }, + "node_modules/react-i18next": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.3.tgz", + "integrity": "sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", @@ -3133,6 +3302,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/webui/package.json b/webui/package.json index 98006bd..d361ade 100644 --- a/webui/package.json +++ b/webui/package.json @@ -10,19 +10,25 @@ }, "dependencies": { "@auth0/nextjs-auth0": "^4.6.1", + "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", + "@emotion/serialize": "^1.3.3", "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.1", + "@mui/material-nextjs": "^7.1.1", "@mui/x-data-grid": "^8.5.2", "@mui/x-date-pickers": "^8.5.2", "@stripe/react-stripe-js": "^3.7.0", "@stripe/stripe-js": "^7.3.1", "axios": "^1.9.0", + "i18next": "^25.2.1", "lucide-react": "^0.516.0", "next": "15.3.3", + "next-i18next": "^15.4.2", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-i18next": "^15.5.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", 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/ClientLayoutEffect.js b/webui/src/app/components/ClientLayoutEffect.js new file mode 100644 index 0000000..9f52a2a --- /dev/null +++ b/webui/src/app/components/ClientLayoutEffect.js @@ -0,0 +1,21 @@ +'use client'; + +import { useEffect } from 'react'; +import axios from 'axios'; + +export default function ClientLayoutEffect() { + useEffect(() => { + async function fetchData() { + try { + const res = await axios.get('/token'); + console.log('Token response:', res.data); + } catch (error) { + console.error('Token fetch error:', error); + } + } + + fetchData().then(r => console.log("Ok")); + }, []); + + return null; +} \ No newline at end of file diff --git a/webui/src/app/components/ImprinkAppBar.js b/webui/src/app/components/ImprinkAppBar.js new file mode 100644 index 0000000..d635d44 --- /dev/null +++ b/webui/src/app/components/ImprinkAppBar.js @@ -0,0 +1,352 @@ +'use client' + +import { + AppBar, + Button, + Toolbar, + Typography, + Avatar, + Box, + IconButton, + Menu, + MenuItem, + Divider, + useMediaQuery, + useTheme, + Paper +} from "@mui/material"; +import { useState } from "react"; +import { useUser } from "@auth0/nextjs-auth0"; +import { + Menu as MenuIcon, + Home, + PhotoLibrary, + ShoppingBag, + Store, + Dashboard, + AdminPanelSettings, + Api, + BugReport +} from "@mui/icons-material"; +import ThemeToggleButton from "@/app/components/theme/ThemeToggleButton"; +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 theme = useTheme(); + const { isDarkMode, toggleTheme } = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const handleMenuOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + const navigationLinks = [ + { label: 'Home', href: '/', icon: , show: true }, + { label: 'Gallery', href: '/gallery', icon: , show: true }, + { label: 'Orders', href: '/orders', icon: , show: true }, + { label: 'Merchant', href: '/merchant', icon: , show: isMerchant }, + ]; + + const adminLinks = [ + { label: 'Dashboard', href: '/dashboard', icon: , show: isMerchant }, + { label: 'Admin', href: '/admin', icon: , show: isAdmin }, + { label: 'Swagger', href: '/swagger', icon: , show: isAdmin }, + { label: 'SEQ', href: '/seq', icon: , show: isAdmin }, + ]; + + const visibleLinks = navigationLinks.filter(link => link.show); + const visibleAdminLinks = adminLinks.filter(link => link.show); + + const renderDesktopNavigation = () => ( + + {visibleLinks.map((link) => ( + + ))} + + ); + + const renderAdminBar = () => { + if (!visibleAdminLinks.length || isMobile) return null; + + return ( + + + + Admin Tools + + {visibleAdminLinks.map((link) => ( + + ))} + + + ); + }; + + const renderMobileMenu = () => ( + <> + + + + + + + {[...visibleLinks, ...visibleAdminLinks].map((link) => ( + + {link.icon} + {link.label} + + ))} + + {!isLoading && ( + <> + + {user ? ( + <> + e.preventDefault()} + > + + + {user.name} + + + e.stopPropagation()}> + + Theme + + + + + + + Logout + + + ) : ( + <> + + Login + + + Sign Up + + + + Theme + + {isDarkMode ? '🌙' : '☀️'} + + + + + )} + + )} + + + ); + + return ( + <> + + + {isMobile && renderMobileMenu()} + + + Imprink + + + {!isMobile && ( + <> + + {renderDesktopNavigation()} + + + + + {isLoading ? ( + + Loading... + + ) : user ? ( + + + + {user.name} + + + + ) : ( + + + + + )} + + )} + + + + {renderAdminBar()} + + ) +} \ No newline at end of file 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/components/theme/MuiThemeProvider.js b/webui/src/app/components/theme/MuiThemeProvider.js new file mode 100644 index 0000000..87678ce --- /dev/null +++ b/webui/src/app/components/theme/MuiThemeProvider.js @@ -0,0 +1,18 @@ +'use client'; + +import { ThemeProvider } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import { darkTheme } from './darkTheme'; +import { lightTheme } from './lightTheme'; +import { useTheme } from './ThemeContext'; + +export default function MuiThemeProvider({ children }) { + const { isDarkMode } = useTheme(); + + return ( + + + {children} + + ); +} \ No newline at end of file diff --git a/webui/src/app/components/theme/ThemeContext.js b/webui/src/app/components/theme/ThemeContext.js new file mode 100644 index 0000000..55017ed --- /dev/null +++ b/webui/src/app/components/theme/ThemeContext.js @@ -0,0 +1,69 @@ +'use client'; + +import { createContext, useContext, useState, useEffect } from 'react'; + +const ThemeContext = createContext({ isDarkMode: true }); + +function getInitialTheme() { + if (typeof window === 'undefined') { + return null; + } + + const savedTheme = localStorage.getItem('theme-preference'); + if (savedTheme) { + return savedTheme === 'dark'; + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +export function ThemeContextProvider({ children }) { + const [isDarkMode, setIsDarkMode] = useState(true); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + const initialTheme = getInitialTheme(); + if (initialTheme !== null) { + setIsDarkMode(initialTheme); + } + setIsInitialized(true); + }, []); + + const toggleTheme = () => { + const newTheme = !isDarkMode; + setIsDarkMode(newTheme); + + if (typeof window !== 'undefined') { + localStorage.setItem('theme-preference', newTheme ? 'dark' : 'light'); + } + }; + + if (!isInitialized) { + return ( +
+ {children} +
+ ); + } + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeContextProvider'); + } + return context; +} \ No newline at end of file diff --git a/webui/src/app/components/theme/ThemeToggleButton.js b/webui/src/app/components/theme/ThemeToggleButton.js new file mode 100644 index 0000000..c228443 --- /dev/null +++ b/webui/src/app/components/theme/ThemeToggleButton.js @@ -0,0 +1,25 @@ +'use client'; + +import { IconButton } from '@mui/material'; +import { useTheme } from './ThemeContext'; + +export default function ThemeToggleButton() { + const { isDarkMode, toggleTheme } = useTheme(); + + return ( + + {isDarkMode ? '🌙' : '☀️'} + + ); +} \ No newline at end of file diff --git a/webui/src/app/components/theme/darkTheme.js b/webui/src/app/components/theme/darkTheme.js new file mode 100644 index 0000000..c20539d --- /dev/null +++ b/webui/src/app/components/theme/darkTheme.js @@ -0,0 +1,279 @@ +'use client' + +import { createTheme } from '@mui/material/styles'; + +export const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#6366f1', + light: '#818cf8', + dark: '#4f46e5', + contrastText: '#ffffff', + }, + secondary: { + main: '#f59e0b', + light: '#fbbf24', + dark: '#d97706', + contrastText: '#000000', + }, + background: { + default: '#0f0f23', + paper: '#1a1a2e', + }, + surface: { + main: '#16213e', + }, + text: { + primary: '#f8fafc', + secondary: '#cbd5e1', + }, + error: { + main: '#ef4444', + light: '#f87171', + dark: '#dc2626', + }, + warning: { + main: '#f59e0b', + light: '#fbbf24', + dark: '#d97706', + }, + info: { + main: '#06b6d4', + light: '#22d3ee', + dark: '#0891b2', + }, + success: { + main: '#10b981', + light: '#34d399', + dark: '#059669', + }, + divider: '#334155', + }, + typography: { + fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + h1: { + fontSize: '2.5rem', + fontWeight: 700, + lineHeight: 1.2, + letterSpacing: '-0.025em', + }, + h2: { + fontSize: '2rem', + fontWeight: 600, + lineHeight: 1.3, + letterSpacing: '-0.025em', + }, + h3: { + fontSize: '1.5rem', + fontWeight: 600, + lineHeight: 1.4, + letterSpacing: '-0.015em', + }, + h4: { + fontSize: '1.25rem', + fontWeight: 600, + lineHeight: 1.4, + }, + h5: { + fontSize: '1.125rem', + fontWeight: 600, + lineHeight: 1.5, + }, + h6: { + fontSize: '1rem', + fontWeight: 600, + lineHeight: 1.5, + }, + body1: { + fontSize: '1rem', + lineHeight: 1.6, + fontWeight: 400, + }, + body2: { + fontSize: '0.875rem', + lineHeight: 1.6, + fontWeight: 400, + }, + button: { + fontSize: '0.875rem', + fontWeight: 500, + textTransform: 'none', + letterSpacing: '0.025em', + }, + }, + shape: { + borderRadius: 12, + }, + shadows: [ + 'none', + '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', + '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + ], + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + scrollbarWidth: 'thin', + scrollbarColor: '#6366f1 #1a1a2e', + '&::-webkit-scrollbar': { + width: '8px', + }, + '&::-webkit-scrollbar-track': { + background: '#1a1a2e', + }, + '&::-webkit-scrollbar-thumb': { + background: '#6366f1', + borderRadius: '4px', + }, + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: '8px', + padding: '10px 24px', + fontSize: '0.875rem', + fontWeight: 500, + boxShadow: 'none', + '&:hover': { + boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', + transform: 'translateY(-1px)', + }, + '&:active': { + transform: 'translateY(0)', + }, + }, + contained: { + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + '&:hover': { + background: 'linear-gradient(135deg, #5b21b6 0%, #7c3aed 100%)', + }, + }, + outlined: { + borderWidth: '1.5px', + '&:hover': { + borderWidth: '1.5px', + backgroundColor: 'rgba(99, 102, 241, 0.08)', + }, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '& .MuiOutlinedInput-root': { + borderRadius: '8px', + backgroundColor: 'rgba(255, 255, 255, 0.02)', + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: '#6366f1', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: '#6366f1', + borderWidth: '2px', + }, + }, + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)', + border: '1px solid rgba(99, 102, 241, 0.1)', + backdropFilter: 'blur(20px)', + '&:hover': { + border: '1px solid rgba(99, 102, 241, 0.2)', + transform: 'translateY(-2px)', + boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)', + }, + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + backgroundColor: '#1a1a2e', + border: '1px solid rgba(99, 102, 241, 0.1)', + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + background: 'rgba(26, 26, 46, 0.8)', + backdropFilter: 'blur(20px)', + borderBottom: '1px solid rgba(99, 102, 241, 0.1)', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', + }, + }, + }, + MuiDrawer: { + styleOverrides: { + paper: { + background: 'linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)', + border: 'none', + borderRight: '1px solid rgba(99, 102, 241, 0.1)', + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + background: 'rgba(99, 102, 241, 0.1)', + color: '#818cf8', + border: '1px solid rgba(99, 102, 241, 0.2)', + '&:hover': { + background: 'rgba(99, 102, 241, 0.2)', + }, + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: 'none', + fontWeight: 500, + fontSize: '0.875rem', + '&.Mui-selected': { + color: '#6366f1', + }, + }, + }, + }, + MuiTabs: { + styleOverrides: { + indicator: { + background: 'linear-gradient(90deg, #6366f1, #8b5cf6)', + height: '3px', + borderRadius: '3px', + }, + }, + }, + }, +}); \ No newline at end of file diff --git a/webui/src/app/components/theme/lightTheme.js b/webui/src/app/components/theme/lightTheme.js new file mode 100644 index 0000000..24088c4 --- /dev/null +++ b/webui/src/app/components/theme/lightTheme.js @@ -0,0 +1,279 @@ +'use client' + +import { createTheme } from '@mui/material/styles'; + +export const lightTheme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#6366f1', + light: '#818cf8', + dark: '#4f46e5', + contrastText: '#ffffff', + }, + secondary: { + main: '#f59e0b', + light: '#fbbf24', + dark: '#d97706', + contrastText: '#000000', + }, + background: { + default: '#f8fafc', + paper: '#ffffff', + }, + surface: { + main: '#f1f5f9', + }, + text: { + primary: '#0f172a', + secondary: '#475569', + }, + error: { + main: '#ef4444', + light: '#f87171', + dark: '#dc2626', + }, + warning: { + main: '#f59e0b', + light: '#fbbf24', + dark: '#d97706', + }, + info: { + main: '#06b6d4', + light: '#22d3ee', + dark: '#0891b2', + }, + success: { + main: '#10b981', + light: '#34d399', + dark: '#059669', + }, + divider: '#e2e8f0', + }, + typography: { + fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + h1: { + fontSize: '2.5rem', + fontWeight: 700, + lineHeight: 1.2, + letterSpacing: '-0.025em', + }, + h2: { + fontSize: '2rem', + fontWeight: 600, + lineHeight: 1.3, + letterSpacing: '-0.025em', + }, + h3: { + fontSize: '1.5rem', + fontWeight: 600, + lineHeight: 1.4, + letterSpacing: '-0.015em', + }, + h4: { + fontSize: '1.25rem', + fontWeight: 600, + lineHeight: 1.4, + }, + h5: { + fontSize: '1.125rem', + fontWeight: 600, + lineHeight: 1.5, + }, + h6: { + fontSize: '1rem', + fontWeight: 600, + lineHeight: 1.5, + }, + body1: { + fontSize: '1rem', + lineHeight: 1.6, + fontWeight: 400, + }, + body2: { + fontSize: '0.875rem', + lineHeight: 1.6, + fontWeight: 400, + }, + button: { + fontSize: '0.875rem', + fontWeight: 500, + textTransform: 'none', + letterSpacing: '0.025em', + }, + }, + shape: { + borderRadius: 12, + }, + shadows: [ + 'none', + '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', + '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + '0 25px 50px -12px rgba(0, 0, 0, 0.15)', + ], + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + scrollbarWidth: 'thin', + scrollbarColor: '#6366f1 #f1f5f9', + '&::-webkit-scrollbar': { + width: '8px', + }, + '&::-webkit-scrollbar-track': { + background: '#f1f5f9', + }, + '&::-webkit-scrollbar-thumb': { + background: '#6366f1', + borderRadius: '4px', + }, + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: '8px', + padding: '10px 24px', + fontSize: '0.875rem', + fontWeight: 500, + boxShadow: 'none', + '&:hover': { + boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', + transform: 'translateY(-1px)', + }, + '&:active': { + transform: 'translateY(0)', + }, + }, + contained: { + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + '&:hover': { + background: 'linear-gradient(135deg, #5b21b6 0%, #7c3aed 100%)', + }, + }, + outlined: { + borderWidth: '1.5px', + '&:hover': { + borderWidth: '1.5px', + backgroundColor: 'rgba(99, 102, 241, 0.08)', + }, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '& .MuiOutlinedInput-root': { + borderRadius: '8px', + backgroundColor: 'rgba(241, 245, 249, 0.5)', + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: '#6366f1', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: '#6366f1', + borderWidth: '2px', + }, + }, + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + background: 'linear-gradient(145deg, #ffffff 0%, #f8fafc 100%)', + border: '1px solid rgba(99, 102, 241, 0.1)', + backdropFilter: 'blur(20px)', + '&:hover': { + border: '1px solid rgba(99, 102, 241, 0.2)', + transform: 'translateY(-2px)', + boxShadow: '0 20px 40px rgba(99, 102, 241, 0.15)', + }, + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + backgroundColor: '#ffffff', + border: '1px solid rgba(99, 102, 241, 0.1)', + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + background: 'rgba(255, 255, 255, 0.8)', + backdropFilter: 'blur(20px)', + borderBottom: '1px solid rgba(99, 102, 241, 0.1)', + boxShadow: '0 8px 32px rgba(99, 102, 241, 0.1)', + color: '#0f172a', + }, + }, + }, + MuiDrawer: { + styleOverrides: { + paper: { + background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)', + border: 'none', + borderRight: '1px solid rgba(99, 102, 241, 0.1)', + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + background: 'rgba(99, 102, 241, 0.7)', + border: '1px solid rgba(99, 102, 241, 0.2)', + '&:hover': { + background: 'rgba(99, 102, 241, 0.9)', + }, + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: 'none', + fontWeight: 500, + fontSize: '0.875rem', + '&.Mui-selected': { + color: '#6366f1', + }, + }, + }, + }, + MuiTabs: { + styleOverrides: { + indicator: { + background: 'linear-gradient(90deg, #6366f1, #8b5cf6)', + height: '3px', + borderRadius: '3px', + }, + }, + }, + }, +}); \ 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/layout.js b/webui/src/app/layout.js index 799eaf5..1ee1f4a 100644 --- a/webui/src/app/layout.js +++ b/webui/src/app/layout.js @@ -1,14 +1,31 @@ -export const metadata = { - title: 'Stripe Payment Demo', - description: 'Stripe payment integration demo with Next.js App Router', -} +import { Inter } from 'next/font/google'; +import MuiThemeProvider from './components/theme/MuiThemeProvider'; +import { ThemeContextProvider } from './components/theme/ThemeContext'; +import { AppRouterCacheProvider } from "@mui/material-nextjs/v13-appRouter"; +import ImprinkAppBar from "@/app/components/ImprinkAppBar"; +import ClientLayoutEffect from "@/app/components/ClientLayoutEffect"; -export default function RootLayout({ children }) { +const inter = Inter({ subsets: ['latin'] }); + +export const metadata = { + title: 'Imprink', + description: 'Turn your dreams into colorful realities!', +}; + +export default function RootLayout({children}) { return ( - - {children} - + + + + + + + {children} + + + + - ) + ); } \ No newline at end of file diff --git a/webui/src/app/page.js b/webui/src/app/page.js index e14237a..3c9238b 100644 --- a/webui/src/app/page.js +++ b/webui/src/app/page.js @@ -1,143 +1,426 @@ 'use client'; -import { useState } from 'react'; -import { loadStripe } from '@stripe/stripe-js'; -import { Elements } from '@stripe/react-stripe-js'; -import PaymentForm from './components/PaymentForm'; -import './globals.css'; +import { + Box, + Container, + Typography, + Button, + Card, + CardContent, + CardMedia, + Grid, + Chip, + CircularProgress, + Alert +} from '@mui/material'; +import { useState, useEffect } from 'react'; +import { ShoppingCart, Palette, ImageOutlined, CreditCard, LocalShipping, CheckCircle } from '@mui/icons-material'; +import clientApi from "@/lib/clientApi"; -const stripePromise = loadStripe(''); +export default function HomePage() { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); -const products = [ - { id: '1', name: 'Premium Widget', price: 2999, description: 'High-quality widget for professionals' }, - { id: '2', name: 'Standard Widget', price: 1999, description: 'Reliable widget for everyday use' }, - { id: '3', name: 'Basic Widget', price: 999, description: 'Entry-level widget for beginners' } -]; - -export default function Home() { - const [selectedProduct, setSelectedProduct] = useState(null); - const [clientSecret, setClientSecret] = useState(''); - const [orderId, setOrderId] = useState(''); - const [loading, setLoading] = useState(false); - - const handleProductSelect = async (product) => { - setLoading(true); - setSelectedProduct(product); - - const newOrderId = Math.floor(Math.random() * 10000).toString(); - setOrderId(newOrderId); - - try { - const response = await fetch('https://impr.ink/api/stripe/create-payment-intent', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - amount: product.price, - orderId: newOrderId - }), - }); - - const data = await response.json(); - - if (data.clientSecret) { - setClientSecret(data.clientSecret); - } else { - console.error('Error creating payment intent:', data.error); + useEffect(() => { + const fetchProducts = async () => { + try { + const response = await clientApi.get('/products/', { + params: { + PageSize: 3, + PageNumber: 1, + IsActive: true, + SortBy: 'Price', + SortDirection: 'DESC' + } + }); + setProducts(response.data.items); + } catch (err) { + setError('Failed to load products'); + console.error('Error fetching products:', err); + } finally { + setLoading(false); } - } catch (error) { - console.error('Error:', error); - } finally { - setLoading(false); - } - }; + }; - const handlePaymentSuccess = () => { - setSelectedProduct(null); - setClientSecret(''); - setOrderId(''); - }; + fetchProducts(); + }, []); - const handleBackToProducts = () => { - setSelectedProduct(null); - setClientSecret(''); - setOrderId(''); - }; - - const appearance = { - theme: 'stripe', - variables: { - colorPrimary: '#0570de', - colorBackground: '#ffffff', - colorText: '#30313d', - colorDanger: '#df1b41', - fontFamily: 'Ideal Sans, system-ui, sans-serif', - spacingUnit: '2px', - borderRadius: '4px', + const steps = [ + { + number: 1, + label: 'Pick an Item', + description: 'Browse our extensive collection of customizable products and select the perfect base for your design. From premium t-shirts and hoodies to mugs, phone cases, and more - we have everything you need to bring your vision to life.', + icon: , + details: 'Explore hundreds of high-quality products across multiple categories. Filter by material, size, color, and price to find exactly what you\'re looking for.' }, - }; - - const options = { - clientSecret, - appearance, - }; + { + number: 2, + label: 'Choose Variant', + description: 'Select from available sizes, colors, and material options that match your preferences and needs. Each product comes with detailed specifications and sizing guides.', + icon: , + details: 'View real-time previews of different variants. Check material quality, durability ratings, and care instructions for each option.' + }, + { + number: 3, + label: 'Customize with Images', + description: 'Upload your own designs, add custom text, or use our intuitive design tools to create something truly unique. Our editor supports various file formats and offers professional design features.', + icon: , + details: 'Drag and drop images, adjust positioning, add filters, create text overlays, and preview your design in real-time on the selected product.' + }, + { + number: 4, + label: 'Pay', + description: 'Complete your order with our secure checkout process. We accept multiple payment methods and provide instant order confirmation with detailed receipts.', + icon: , + details: 'Review your design, confirm quantities, apply discount codes, and choose from various secure payment options including cards, PayPal, and more.' + }, + { + number: 5, + label: 'Wait for Order', + description: 'Sit back and relax while we handle the rest. Our professional printing team will carefully produce your custom item and ship it directly to your door.', + icon: , + details: 'Track your order status in real-time, from printing to packaging to shipping. Receive updates via email and SMS throughout the process.' + } + ]; return ( -
-
-

🛍️ Stripe Payment Demo

-

Select a product to purchase

-
+ + + + + + Custom Printing
+ Made Simple +
+ + Transform your ideas into reality with our premium custom printing services. + From t-shirts to mugs, we bring your designs to life with professional quality + and lightning-fast turnaround times. + - {!selectedProduct ? ( -
-

Products

-
+ + + + + + + Professional Quality Guaranteed + + + + + + + + Fast 24-48 Hour Turnaround + + + + + + + + Free Design Support + + + + + + + + 100% Satisfaction Promise + + + + + + + + + + + + ⭐⭐⭐⭐⭐ Trusted by 10,000+ customers • 4.9/5 rating + + + + + + + + + Featured Products + + + Discover our most popular customizable products. Each item is carefully selected + for quality and perfect for personalization. + + + + {loading && ( + + + + )} + + {error && ( + + {error} + + )} + + {!loading && !error && ( + {products.map((product) => ( -
-

{product.name}

-

{product.description}

-

${(product.price / 100).toFixed(2)}

- -
+ + + + + {product.name} + + + {product.description} + + + + From ${product.basePrice?.toFixed(2)} + + {product.isCustomizable && ( + + )} + + + + ))} -
-
- ) : ( -
-
-

Order Summary

-
-

Product: {selectedProduct.name}

-

Order ID: {orderId}

-

Amount: ${(selectedProduct.price / 100).toFixed(2)}

-
-
+ + )} + - {clientSecret && ( - - - - )} + + + + + How It Works + + + Our streamlined process makes custom printing simple and stress-free. + Follow these five easy steps to get your perfect custom products. + + - -
- )} -
+ + {steps.map((step, index) => ( + + + + + + + {step.icon} + + + {step.number} + + + + + + + + {step.label} + + + {step.description} + + + {step.details} + + + + + + + ))} + + + + + + + Ready to Get Started? + + + Join thousands of satisfied customers who trust us with their custom printing needs. + Quality guaranteed, fast turnaround, and competitive prices. + + + + ); } \ No newline at end of file diff --git a/webui/src/app/token/route.js b/webui/src/app/token/route.js index a3fad83..9ffd1a3 100644 --- a/webui/src/app/token/route.js +++ b/webui/src/app/token/route.js @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import {auth0} from "@/lib/auth0"; -import api from "@/lib/api"; +import serverApi from "@/lib/serverApi"; export async function GET() { try { @@ -8,13 +8,13 @@ export async function GET() { if (!token) { return NextResponse.json({ error: 'No access token found' }, { status: 401 }); } - await api.post('/users/sync', {}, { - headers: { Cookie: `access_token=${token}` } + await serverApi.post('/users/me/sync', null, { + headers: { Authorization: `Bearer ${token}`} }); - return NextResponse.json({ access_token: token }); + return NextResponse.json("Ok"); } catch (error) { - console.error('Error in /api/token:', error); + console.error('Error in /serverApi/token:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } \ No newline at end of file diff --git a/webui/src/lib/clientApi.js b/webui/src/lib/clientApi.js new file mode 100644 index 0000000..de600a8 --- /dev/null +++ b/webui/src/lib/clientApi.js @@ -0,0 +1,31 @@ +import axios from "axios"; + +const clientApi = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + withCredentials: true, +}); + +clientApi.interceptors.request.use(async (config) => { + if (typeof window === 'undefined') return config; + + try { + const res = await fetch('/auth/access-token'); + if (!res.ok) + throw new Error('Failed to fetch token'); + const data = await res.json(); + + if (data.token) { + config.headers.Authorization = `Bearer ${data.token}`; + } else { + console.warn('No token received from /auth/access-token'); + } + } catch (err) { + console.error('Error fetching token:', err); + } + + return config; +}, error => { + return Promise.reject(error); +}); + +export default clientApi; \ No newline at end of file diff --git a/webui/src/lib/api.js b/webui/src/lib/serverApi.js similarity index 64% rename from webui/src/lib/api.js rename to webui/src/lib/serverApi.js index 0b1cb3c..5371f2f 100644 --- a/webui/src/lib/api.js +++ b/webui/src/lib/serverApi.js @@ -1,8 +1,8 @@ import axios from "axios"; -const api = axios.create({ +const serverApi = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, withCredentials: true, }); -export default api; \ No newline at end of file +export default serverApi; \ No newline at end of file