Add MUI theming and an axios interceptor

This commit is contained in:
lumijiez
2025-06-23 20:39:46 +03:00
parent d2afe2df98
commit d4f01c3f73
13 changed files with 949 additions and 154 deletions

View File

@@ -19,4 +19,8 @@
<ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" /> <ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Commands\Orders\" />
</ItemGroup>
</Project> </Project>

View File

@@ -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 (
<div className="container">
<header>
<h1>🛍 Stripe Payment Demo</h1>
<p>Select a product to purchase</p>
</header>
{!selectedProduct ? (
<div className="products">
<h2>Products</h2>
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p className="description">{product.description}</p>
<p className="price">${(product.price / 100).toFixed(2)}</p>
<button
onClick={() => handleProductSelect(product)}
disabled={loading}
className="select-btn"
>
{loading ? 'Loading...' : 'Select'}
</button>
</div>
))}
</div>
</div>
) : (
<div className="checkout">
<div className="order-summary">
<h2>Order Summary</h2>
<div className="order-details">
<p><strong>Product:</strong> {selectedProduct.name}</p>
<p><strong>Order ID:</strong> {orderId}</p>
<p><strong>Amount:</strong> ${(selectedProduct.price / 100).toFixed(2)}</p>
</div>
</div>
{clientSecret && (
<Elements options={options} stripe={stripePromise}>
<PaymentForm
onSuccess={handlePaymentSuccess}
orderId={orderId}
/>
</Elements>
)}
<button
onClick={handleBackToProducts}
className="back-btn"
>
Back to Products
</button>
</div>
)}
</div>
);
}

View File

@@ -1,14 +1,4 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {}
webpack: (config, { dev }) => {
if (dev) {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
}
}
return config
},
}
export default nextConfig; export default nextConfig;

View File

@@ -9,10 +9,13 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@auth0/nextjs-auth0": "^4.6.1", "@auth0/nextjs-auth0": "^4.6.1",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.1", "@mui/icons-material": "^7.1.1",
"@mui/material": "^7.1.1", "@mui/material": "^7.1.1",
"@mui/material-nextjs": "^7.1.1",
"@mui/x-data-grid": "^8.5.2", "@mui/x-data-grid": "^8.5.2",
"@mui/x-date-pickers": "^8.5.2", "@mui/x-date-pickers": "^8.5.2",
"@stripe/react-stripe-js": "^3.7.0", "@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": { "node_modules/@mui/private-theming": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz",

View File

@@ -10,10 +10,13 @@
}, },
"dependencies": { "dependencies": {
"@auth0/nextjs-auth0": "^4.6.1", "@auth0/nextjs-auth0": "^4.6.1",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.1", "@mui/icons-material": "^7.1.1",
"@mui/material": "^7.1.1", "@mui/material": "^7.1.1",
"@mui/material-nextjs": "^7.1.1",
"@mui/x-data-grid": "^8.5.2", "@mui/x-data-grid": "^8.5.2",
"@mui/x-date-pickers": "^8.5.2", "@mui/x-date-pickers": "^8.5.2",
"@stripe/react-stripe-js": "^3.7.0", "@stripe/react-stripe-js": "^3.7.0",

View File

@@ -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 (
<IconButton
onClick={toggleTheme}
color="inherit"
sx={{
width: 40,
height: 40,
borderRadius: '8px',
'&:hover': {
backgroundColor: 'rgba(99, 102, 241, 0.1)',
},
}}
>
{isDarkMode ? '🌙' : '☀️'}
</IconButton>
);
}

View File

@@ -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 (
<ThemeProvider theme={isDarkMode ? darkTheme : lightTheme}>
<CssBaseline />
{children}
</ThemeProvider>
);
}

View File

@@ -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 (
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeContextProvider');
}
return context;
}

View File

@@ -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',
},
},
},
},
});

View File

@@ -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',
},
},
},
},
});

View File

@@ -1,14 +1,27 @@
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";
const inter = Inter({ subsets: ['latin'] });
export const metadata = { export const metadata = {
title: 'Stripe Payment Demo', title: 'Imprink',
description: 'Stripe payment integration demo with Next.js App Router', description: 'Turn your dreams into colorful realities!',
} };
export default function RootLayout({children}) { export default function RootLayout({children}) {
return ( return (
<html lang="en"> <html lang="en">
<body> <body className={inter.className}>
<AppRouterCacheProvider>
<ThemeContextProvider>
<MuiThemeProvider>
{children} {children}
</MuiThemeProvider>
</ThemeContextProvider>
</AppRouterCacheProvider>
</body> </body>
</html> </html>
) );
} }

View File

@@ -1,143 +1,105 @@
'use client'; 'use client';
import { useState } from 'react'; import {
import { loadStripe } from '@stripe/stripe-js'; Box,
import { Elements } from '@stripe/react-stripe-js'; Container,
import PaymentForm from './components/PaymentForm'; Typography,
import './globals.css'; Button,
Card,
const stripePromise = loadStripe(''); CardContent,
TextField,
const products = [ Chip,
{ id: '1', name: 'Premium Widget', price: 2999, description: 'High-quality widget for professionals' }, AppBar,
{ id: '2', name: 'Standard Widget', price: 1999, description: 'Reliable widget for everyday use' }, Toolbar,
{ id: '3', name: 'Basic Widget', price: 999, description: 'Entry-level widget for beginners' } Grid,
]; } from '@mui/material';
import ThemeToggleButton from './components/ThemeToggleButton';
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,
};
export default function HomePage() {
return ( return (
<div className="container"> <>
<header> <AppBar position="static">
<h1>🛍 Stripe Payment Demo</h1> <Toolbar>
<p>Select a product to purchase</p> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
</header> Modern App
</Typography>
<ThemeToggleButton />
<Button color="inherit" sx={{ ml: 2 }}>Login</Button>
</Toolbar>
</AppBar>
{!selectedProduct ? ( <Container maxWidth="lg" sx={{ py: 4 }}>
<div className="products"> <Box sx={{ mb: 6, textAlign: 'center' }}>
<h2>Products</h2> <Typography variant="h1" gutterBottom>
<div className="product-grid"> Welcome to Modern UI
{products.map((product) => ( </Typography>
<div key={product.id} className="product-card"> <Typography variant="h5" color="text.secondary" sx={{ mb: 4 }}>
<h3>{product.name}</h3> Experience the beauty of modern Material-UI theming
<p className="description">{product.description}</p> </Typography>
<p className="price">${(product.price / 100).toFixed(2)}</p> <Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
<button <Button variant="contained" size="large">
onClick={() => handleProductSelect(product)} Get Started
disabled={loading} </Button>
className="select-btn" <Button variant="outlined" size="large">
> Learn More
{loading ? 'Loading...' : 'Select'} </Button>
</button> </Box>
</div> </Box>
))}
</div>
</div>
) : (
<div className="checkout">
<div className="order-summary">
<h2>Order Summary</h2>
<div className="order-details">
<p><strong>Product:</strong> {selectedProduct.name}</p>
<p><strong>Order ID:</strong> {orderId}</p>
<p><strong>Amount:</strong> ${(selectedProduct.price / 100).toFixed(2)}</p>
</div>
</div>
{clientSecret && ( <Grid container spacing={4}>
<Elements options={options} stripe={stripePromise}> <Grid item xs={12} md={6}>
<PaymentForm <Card>
onSuccess={handlePaymentSuccess} <CardContent>
orderId={orderId} <Typography variant="h4" gutterBottom>
Beautiful Cards
</Typography>
<Typography variant="body1" color="text.secondary" paragraph>
Cards with modern gradients, hover effects, and perfect spacing that make your content shine.
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
<Chip label="Modern" />
<Chip label="Responsive" />
<Chip label="Beautiful" />
</Box>
<Button variant="contained">Explore</Button>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h4" gutterBottom>
Form Elements
</Typography>
<Box component="form" sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<TextField
label="Your Name"
variant="outlined"
fullWidth
/> />
</Elements> <TextField
)} label="Email Address"
variant="outlined"
<button type="email"
onClick={handleBackToProducts} fullWidth
className="back-btn" />
> <TextField
Back to Products label="Message"
</button> variant="outlined"
</div> multiline
)} rows={4}
</div> fullWidth
/>
<Button variant="contained" size="large">
Send Message
</Button>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
</>
); );
} }

View File

@@ -5,4 +5,18 @@ const api = axios.create({
withCredentials: true, 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; export default api;