From d4f01c3f73cb7cc9eef4833cc53a4c68e72b2164 Mon Sep 17 00:00:00 2001
From: lumijiez <59575049+lumijiez@users.noreply.github.com>
Date: Mon, 23 Jun 2025 20:39:46 +0300
Subject: [PATCH 1/6] Add MUI theming and an axios interceptor
---
.../Imprink.Application.csproj | 4 +
webui/auth0-templates/page.js | 143 +++++++++
webui/next.config.mjs | 12 +-
webui/package-lock.json | 38 +++
webui/package.json | 3 +
webui/src/app/components/ThemeToggleButton.js | 25 ++
.../app/components/theme/MuiThemeProvider.js | 18 ++
.../src/app/components/theme/ThemeContext.js | 27 ++
webui/src/app/components/theme/darkTheme.js | 279 ++++++++++++++++++
webui/src/app/components/theme/lightTheme.js | 279 ++++++++++++++++++
webui/src/app/layout.js | 31 +-
webui/src/app/page.js | 230 ++++++---------
webui/src/lib/api.js | 14 +
13 files changed, 949 insertions(+), 154 deletions(-)
create mode 100644 webui/auth0-templates/page.js
create mode 100644 webui/src/app/components/ThemeToggleButton.js
create mode 100644 webui/src/app/components/theme/MuiThemeProvider.js
create mode 100644 webui/src/app/components/theme/ThemeContext.js
create mode 100644 webui/src/app/components/theme/darkTheme.js
create mode 100644 webui/src/app/components/theme/lightTheme.js
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/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 (
+
+
+
+ {!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..dd9ee23 100644
--- a/webui/next.config.mjs
+++ b/webui/next.config.mjs
@@ -1,14 +1,4 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {
- webpack: (config, { dev }) => {
- if (dev) {
- config.watchOptions = {
- poll: 1000,
- aggregateTimeout: 300,
- }
- }
- return config
- },
-}
+const nextConfig = {}
export default nextConfig;
\ No newline at end of file
diff --git a/webui/package-lock.json b/webui/package-lock.json
index dfadc04..6befedc 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -9,10 +9,13 @@
"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",
@@ -910,6 +913,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",
diff --git a/webui/package.json b/webui/package.json
index 98006bd..4db9571 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -10,10 +10,13 @@
},
"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",
diff --git a/webui/src/app/components/ThemeToggleButton.js b/webui/src/app/components/ThemeToggleButton.js
new file mode 100644
index 0000000..54f0f8d
--- /dev/null
+++ b/webui/src/app/components/ThemeToggleButton.js
@@ -0,0 +1,25 @@
+'use client';
+
+import { IconButton } from '@mui/material';
+import { useTheme } from './theme/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/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..ae25725
--- /dev/null
+++ b/webui/src/app/components/theme/ThemeContext.js
@@ -0,0 +1,27 @@
+'use client';
+
+import { createContext, useContext, useState } from 'react';
+
+const ThemeContext = createContext({ isDarkMode: true });
+
+export function ThemeContextProvider({ children }) {
+ const [isDarkMode, setIsDarkMode] = useState(true);
+
+ const toggleTheme = () => {
+ setIsDarkMode(!isDarkMode);
+ };
+
+ 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/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..39592b1
--- /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.1)',
+ 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/layout.js b/webui/src/app/layout.js
index 799eaf5..3e9fc1e 100644
--- a/webui/src/app/layout.js
+++ b/webui/src/app/layout.js
@@ -1,14 +1,27 @@
-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";
-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..e3f550b 100644
--- a/webui/src/app/page.js
+++ b/webui/src/app/page.js
@@ -1,143 +1,105 @@
'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,
- };
+import {
+ Box,
+ Container,
+ Typography,
+ Button,
+ Card,
+ CardContent,
+ TextField,
+ Chip,
+ AppBar,
+ Toolbar,
+ Grid,
+} from '@mui/material';
+import ThemeToggleButton from './components/ThemeToggleButton';
+export default function HomePage() {
return (
-
-
+ <>
+
+
+
+ Modern App
+
+
+
+
+
- {!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)}
-
-
+
+
+
+ Welcome to Modern UI
+
+
+ Experience the beauty of modern Material-UI theming
+
+
+
+
+
+
- {clientSecret && (
-
-
-
- )}
+
+
+
+
+
+ Beautiful Cards
+
+
+ Cards with modern gradients, hover effects, and perfect spacing that make your content shine.
+
+
+
+
+
+
+
+
+
+
-
-
- )}
-
+
+
+
+
+ Form Elements
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
}
\ No newline at end of file
diff --git a/webui/src/lib/api.js b/webui/src/lib/api.js
index 0b1cb3c..8cf8595 100644
--- a/webui/src/lib/api.js
+++ b/webui/src/lib/api.js
@@ -5,4 +5,18 @@ const api = axios.create({
withCredentials: true,
});
+api.interceptors.request.use((config) => {
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('access_token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ } else {
+ console.log("Token not found in localStorage, please auth!");
+ }
+ }
+ return config;
+}, (error) => {
+ return Promise.reject(error);
+});
+
export default api;
\ No newline at end of file
From a2d8a1e08075467ee6d516f69ab8a3a53fa222be Mon Sep 17 00:00:00 2001
From: lumijiez <59575049+lumijiez@users.noreply.github.com>
Date: Mon, 23 Jun 2025 22:21:48 +0300
Subject: [PATCH 2/6] Connect auth to backend and persist theme preference
---
.../PaymentForm.js | 0
.../src/app/components/ClientLayoutEffect.js | 21 ++++++
webui/src/app/components/ImprinkAppBar.js | 66 +++++++++++++++++++
.../src/app/components/theme/ThemeContext.js | 46 ++++++++++++-
.../{ => theme}/ThemeToggleButton.js | 2 +-
webui/src/app/layout.js | 4 ++
webui/src/app/page.js | 16 +----
webui/src/app/token/route.js | 10 +--
webui/src/lib/api.js | 22 -------
webui/src/lib/clientApi.js | 31 +++++++++
webui/src/lib/serverApi.js | 8 +++
11 files changed, 181 insertions(+), 45 deletions(-)
rename webui/{src/app/components => auth0-templates}/PaymentForm.js (100%)
create mode 100644 webui/src/app/components/ClientLayoutEffect.js
create mode 100644 webui/src/app/components/ImprinkAppBar.js
rename webui/src/app/components/{ => theme}/ThemeToggleButton.js (92%)
delete mode 100644 webui/src/lib/api.js
create mode 100644 webui/src/lib/clientApi.js
create mode 100644 webui/src/lib/serverApi.js
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/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..45587fd
--- /dev/null
+++ b/webui/src/app/components/ImprinkAppBar.js
@@ -0,0 +1,66 @@
+'use client'
+
+import {AppBar, Button, Toolbar, Typography, Avatar, Box} from "@mui/material";
+import { useUser } from "@auth0/nextjs-auth0";
+import ThemeToggleButton from "@/app/components/theme/ThemeToggleButton";
+
+export default function ImprinkAppBar() {
+ const { user, error, isLoading } = useUser();
+
+ return (
+
+
+
+ Modern App
+
+
+
+ {isLoading ? (
+
+ Loading...
+
+ ) : user ? (
+
+
+
+ {user.name}
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/webui/src/app/components/theme/ThemeContext.js b/webui/src/app/components/theme/ThemeContext.js
index ae25725..55017ed 100644
--- a/webui/src/app/components/theme/ThemeContext.js
+++ b/webui/src/app/components/theme/ThemeContext.js
@@ -1,16 +1,58 @@
'use client';
-import { createContext, useContext, useState } from 'react';
+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 = () => {
- setIsDarkMode(!isDarkMode);
+ const newTheme = !isDarkMode;
+ setIsDarkMode(newTheme);
+
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('theme-preference', newTheme ? 'dark' : 'light');
+ }
};
+ if (!isInitialized) {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
{children}
diff --git a/webui/src/app/components/ThemeToggleButton.js b/webui/src/app/components/theme/ThemeToggleButton.js
similarity index 92%
rename from webui/src/app/components/ThemeToggleButton.js
rename to webui/src/app/components/theme/ThemeToggleButton.js
index 54f0f8d..c228443 100644
--- a/webui/src/app/components/ThemeToggleButton.js
+++ b/webui/src/app/components/theme/ThemeToggleButton.js
@@ -1,7 +1,7 @@
'use client';
import { IconButton } from '@mui/material';
-import { useTheme } from './theme/ThemeContext';
+import { useTheme } from './ThemeContext';
export default function ThemeToggleButton() {
const { isDarkMode, toggleTheme } = useTheme();
diff --git a/webui/src/app/layout.js b/webui/src/app/layout.js
index 3e9fc1e..1ee1f4a 100644
--- a/webui/src/app/layout.js
+++ b/webui/src/app/layout.js
@@ -2,6 +2,8 @@ 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";
const inter = Inter({ subsets: ['latin'] });
@@ -17,6 +19,8 @@ export default function RootLayout({children}) {
+
+
{children}
diff --git a/webui/src/app/page.js b/webui/src/app/page.js
index e3f550b..661b9d1 100644
--- a/webui/src/app/page.js
+++ b/webui/src/app/page.js
@@ -9,25 +9,12 @@ import {
CardContent,
TextField,
Chip,
- AppBar,
- Toolbar,
Grid,
} from '@mui/material';
-import ThemeToggleButton from './components/ThemeToggleButton';
export default function HomePage() {
- return (
- <>
-
-
-
- Modern App
-
-
-
-
-
+ return (
@@ -100,6 +87,5 @@ export default function HomePage() {
- >
);
}
\ 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/api.js b/webui/src/lib/api.js
deleted file mode 100644
index 8cf8595..0000000
--- a/webui/src/lib/api.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import axios from "axios";
-
-const api = axios.create({
- baseURL: process.env.NEXT_PUBLIC_API_URL,
- withCredentials: true,
-});
-
-api.interceptors.request.use((config) => {
- if (typeof window !== 'undefined') {
- const token = localStorage.getItem('access_token');
- if (token) {
- config.headers.Authorization = `Bearer ${token}`;
- } else {
- console.log("Token not found in localStorage, please auth!");
- }
- }
- return config;
-}, (error) => {
- return Promise.reject(error);
-});
-
-export default api;
\ 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/serverApi.js b/webui/src/lib/serverApi.js
new file mode 100644
index 0000000..5371f2f
--- /dev/null
+++ b/webui/src/lib/serverApi.js
@@ -0,0 +1,8 @@
+import axios from "axios";
+
+const serverApi = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
+ withCredentials: true,
+});
+
+export default serverApi;
\ No newline at end of file
From 5c7e5cb25348fb1a39aa9721c1e21d4f0a54dbe4 Mon Sep 17 00:00:00 2001
From: lumijiez <59575049+lumijiez@users.noreply.github.com>
Date: Tue, 24 Jun 2025 00:05:37 +0300
Subject: [PATCH 3/6] Landing page
---
webui/src/app/page.js | 316 +++++++++++++++++++++++++++++++++---------
1 file changed, 247 insertions(+), 69 deletions(-)
diff --git a/webui/src/app/page.js b/webui/src/app/page.js
index 661b9d1..5c0aeff 100644
--- a/webui/src/app/page.js
+++ b/webui/src/app/page.js
@@ -7,85 +7,263 @@ import {
Button,
Card,
CardContent,
- TextField,
- Chip,
+ CardMedia,
Grid,
+ Chip,
+ CircularProgress,
+ Alert
} from '@mui/material';
+import { useState, useEffect } from 'react';
+import { ShoppingCart, Palette, CreditCard, LocalShipping, CheckCircle } from '@mui/icons-material';
+import clientApi from "@/lib/clientApi";
export default function HomePage() {
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchProducts = async () => {
+ try {
+ const response = await clientApi.get('/products/', {
+ params: {
+ PageSize: 6,
+ PageNumber: 1,
+ IsActive: true,
+ IsCustomizable: 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);
+ }
+ };
+
+ fetchProducts();
+ }, []);
+
+ const steps = [
+ {
+ label: 'Pick an Item',
+ description: 'Browse our collection of customizable products and select the perfect base for your design.',
+ icon: ,
+ color: '#1976d2'
+ },
+ {
+ label: 'Choose Variant',
+ description: 'Select size, color, and material options that match your preferences and needs.',
+ icon: ,
+ color: '#9c27b0'
+ },
+ {
+ label: 'Customize with Images',
+ description: 'Upload your designs, add text, or use our design tools to create something unique.',
+ icon: ,
+ color: '#f57c00'
+ },
+ {
+ label: 'Pay',
+ description: 'Secure checkout with multiple payment options. Review your order before finalizing.',
+ icon: ,
+ color: '#388e3c'
+ },
+ {
+ label: 'Wait for Order',
+ description: 'We\'ll print and ship your custom item. Track your order status in real-time.',
+ icon: ,
+ color: '#d32f2f'
+ }
+ ];
return (
-
-
-
- Welcome to Modern UI
-
-
- Experience the beauty of modern Material-UI theming
-
-
-
-
-
+
+
+
+ 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.
+
+
+
+
+
-
-
-
-
-
- Beautiful Cards
-
-
- Cards with modern gradients, hover effects, and perfect spacing that make your content shine.
-
-
-
-
-
-
-
-
-
-
+
+
+ Featured Products
+
-
-
-
-
- Form Elements
-
-
-
+
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!loading && !error && (
+
+ {products.map((product) => (
+
+
+
-
-
-
-
-
-
+
+
+ {product.name}
+
+
+ {product.description}
+
+
+
+ From ${product.basePrice?.toFixed(2)}
+
+ {product.isCustomizable && (
+
+ )}
+
+
+
+
+
+ ))}
-
-
+ )}
+
+
+
+
+ How It Works
+
+
+
+ {steps.map((step, index) => (
+
+ {index < steps.length - 1 && (
+
+ )}
+
+
+ {step.icon}
+
+
+
+
+ {index + 1}. {step.label}
+
+
+ {step.description}
+
+
+
+ ))}
+
+
+
+
+
+ Ready to Get Started?
+
+
+ Join thousands of satisfied customers who trust us with their custom printing needs.
+ Quality guaranteed, fast turnaround, and competitive prices.
+
+ }
+ >
+ Start Your Order Today
+
+
+
);
}
\ No newline at end of file
From 4dd71382bb1d7b7b1f26e6a2b099b2aad6b7ba7e Mon Sep 17 00:00:00 2001
From: lumijiez <59575049+lumijiez@users.noreply.github.com>
Date: Tue, 24 Jun 2025 01:08:26 +0300
Subject: [PATCH 4/6] Landing page good for mobiles
---
.../Controllers/SeedingController.cs | 1 -
src/Imprink.WebApi/Seeder.cs | 4 -
webui/next.config.mjs | 3 +-
webui/package-lock.json | 144 +++++-
webui/package.json | 5 +-
webui/src/app/components/theme/lightTheme.js | 4 +-
webui/src/app/page.js | 468 ++++++++++++------
7 files changed, 462 insertions(+), 167 deletions(-)
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/next.config.mjs b/webui/next.config.mjs
index dd9ee23..7859408 100644
--- a/webui/next.config.mjs
+++ b/webui/next.config.mjs
@@ -1,4 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
-export default nextConfig;
\ No newline at end of file
+export default nextConfig;
+
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 6befedc..2270b2a 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -21,10 +21,13 @@
"@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",
@@ -1688,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",
@@ -1705,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"
}
@@ -1892,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",
@@ -2239,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",
@@ -2743,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",
@@ -2913,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",
@@ -3171,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 4db9571..d361ade 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -22,10 +22,13 @@
"@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/src/app/components/theme/lightTheme.js b/webui/src/app/components/theme/lightTheme.js
index 39592b1..24088c4 100644
--- a/webui/src/app/components/theme/lightTheme.js
+++ b/webui/src/app/components/theme/lightTheme.js
@@ -246,10 +246,10 @@ export const lightTheme = createTheme({
MuiChip: {
styleOverrides: {
root: {
- background: 'rgba(99, 102, 241, 0.1)',
+ background: 'rgba(99, 102, 241, 0.7)',
border: '1px solid rgba(99, 102, 241, 0.2)',
'&:hover': {
- background: 'rgba(99, 102, 241, 0.2)',
+ background: 'rgba(99, 102, 241, 0.9)',
},
},
},
diff --git a/webui/src/app/page.js b/webui/src/app/page.js
index 5c0aeff..4f50d30 100644
--- a/webui/src/app/page.js
+++ b/webui/src/app/page.js
@@ -14,7 +14,7 @@ import {
Alert
} from '@mui/material';
import { useState, useEffect } from 'react';
-import { ShoppingCart, Palette, CreditCard, LocalShipping, CheckCircle } from '@mui/icons-material';
+import { ShoppingCart, Palette, ImageOutlined, CreditCard, LocalShipping, CheckCircle } from '@mui/icons-material';
import clientApi from "@/lib/clientApi";
export default function HomePage() {
@@ -27,10 +27,9 @@ export default function HomePage() {
try {
const response = await clientApi.get('/products/', {
params: {
- PageSize: 6,
+ PageSize: 3,
PageNumber: 1,
IsActive: true,
- IsCustomizable: true,
SortBy: 'Price',
SortDirection: 'DESC'
}
@@ -49,61 +48,138 @@ export default function HomePage() {
const steps = [
{
+ number: 1,
label: 'Pick an Item',
- description: 'Browse our collection of customizable products and select the perfect base for your design.',
- icon: ,
- color: '#1976d2'
+ 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.'
},
{
+ number: 2,
label: 'Choose Variant',
- description: 'Select size, color, and material options that match your preferences and needs.',
- icon: ,
- color: '#9c27b0'
+ 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 designs, add text, or use our design tools to create something unique.',
- icon: ,
- color: '#f57c00'
+ 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: 'Secure checkout with multiple payment options. Review your order before finalizing.',
- icon: ,
- color: '#388e3c'
+ 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: 'We\'ll print and ship your custom item. Track your order status in real-time.',
- icon: ,
- color: '#d32f2f'
+ 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 (
-
-
- 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.
-
-
-
-
-
-
+
+
+
+
+ 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.
+
-
-
- Featured 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 && (
@@ -118,136 +194,216 @@ export default function HomePage() {
)}
{!loading && !error && (
-
+
{products.map((product) => (
-
+
+
+
+
+ {product.name}
+
+
+ {product.description}
+
+
+
+ From ${product.basePrice?.toFixed(2)}
+
+ {product.isCustomizable && (
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ 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) => (
+
-
-
-
- {product.name}
-
-
- {product.description}
-
-
-
- From ${product.basePrice?.toFixed(2)}
-
- {product.isCustomizable && (
-
- )}
-
-
-
+
+
+
+
+ {step.icon}
+
+
+ {step.number}
+
+
+
+
+
+
+
+ {step.label}
+
+
+ {step.description}
+
+
+ {step.details}
+
+
+
+
))}
- )}
+
-
-
- How It Works
-
-
-
- {steps.map((step, index) => (
-
- {index < steps.length - 1 && (
-
- )}
-
-
- {step.icon}
-
-
-
-
- {index + 1}. {step.label}
-
-
- {step.description}
-
-
-
- ))}
-
-
-
-
+
Ready to Get Started?
From 8940ad1caf1d6442d74444cece2cd1df875b5739 Mon Sep 17 00:00:00 2001
From: lumijiez <59575049+lumijiez@users.noreply.github.com>
Date: Tue, 24 Jun 2025 01:39:33 +0300
Subject: [PATCH 5/6] Better app bar, mobile, and role based render
---
webui/src/app/components/ImprinkAppBar.js | 411 +++++++++++++++++++---
1 file changed, 364 insertions(+), 47 deletions(-)
diff --git a/webui/src/app/components/ImprinkAppBar.js b/webui/src/app/components/ImprinkAppBar.js
index 45587fd..5ab6649 100644
--- a/webui/src/app/components/ImprinkAppBar.js
+++ b/webui/src/app/components/ImprinkAppBar.js
@@ -1,66 +1,383 @@
'use client'
-import {AppBar, Button, Toolbar, Typography, Avatar, Box} from "@mui/material";
+import {
+ AppBar,
+ Button,
+ Toolbar,
+ Typography,
+ Avatar,
+ Box,
+ IconButton,
+ Menu,
+ MenuItem,
+ Divider,
+ useMediaQuery,
+ useTheme,
+ Paper
+} from "@mui/material";
+import { useState, useEffect } 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 clientApi from "@/lib/clientApi";
export default function ImprinkAppBar() {
const { user, error, isLoading } = useUser();
+ 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'));
- return (
-
-
-
- Modern App
-
-
+ useEffect(() => {
+ const fetchUserRoles = async () => {
+ if (!user) {
+ setUserRoles([]);
+ return;
+ }
- {isLoading ? (
-
- Loading...
-
- ) : user ? (
-
-
-
- {user.name}
-
+ 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);
+ };
+
+ const handleMenuClose = () => {
+ setAnchorEl(null);
+ };
+
+ // Regular navigation links (excluding admin-specific ones)
+ 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 },
+ ];
+
+ // Admin-specific links for the separate bar
+ 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 = () => (
+ <>
+
+
+
+
-
+
+ >
+ );
+
+ return (
+ <>
+
+
+ {isMobile && renderMobileMenu()}
+
+
+ Imprink
+
+
+ {!isMobile && (
+ <>
+
+ {renderDesktopNavigation()}
+
+
+
+
+ {isLoading ? (
+
+ Loading...
+
+ ) : user ? (
+
+
+
+ {user.name}
+
+
+
+ ) : (
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Admin Bar - appears below main app bar */}
+ {renderAdminBar()}
+ >
)
}
\ No newline at end of file
From a74b99ada7f1ae24be7a72357b32849ada44fb20 Mon Sep 17 00:00:00 2001
From: lumijiez <59575049+lumijiez@users.noreply.github.com>
Date: Tue, 24 Jun 2025 04:56:35 +0300
Subject: [PATCH 6/6] Gallery page, somewhat workable on mobile
---
webui/public/file.svg | 1 -
webui/public/globe.svg | 1 -
webui/public/logo.png | Bin 0 -> 1078774 bytes
webui/public/next.svg | 1 -
webui/public/vercel.svg | 1 -
webui/public/window.svg | 1 -
webui/src/app/components/ImprinkAppBar.js | 37 +-
webui/src/app/components/hooks/useRoles.js | 68 +++
webui/src/app/gallery/page.js | 661 +++++++++++++++++++++
webui/src/app/page.js | 1 +
10 files changed, 733 insertions(+), 39 deletions(-)
delete mode 100644 webui/public/file.svg
delete mode 100644 webui/public/globe.svg
create mode 100644 webui/public/logo.png
delete mode 100644 webui/public/next.svg
delete mode 100644 webui/public/vercel.svg
delete mode 100644 webui/public/window.svg
create mode 100644 webui/src/app/components/hooks/useRoles.js
create mode 100644 webui/src/app/gallery/page.js
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 0000000000000000000000000000000000000000..b0ae2d4e095a9f6b2fc1d1a6ab303466214a52d5
GIT binary patch
literal 1078774
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