Add MUI theming and an axios interceptor
This commit is contained in:
143
webui/auth0-templates/page.js
Normal file
143
webui/auth0-templates/page.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
38
webui/package-lock.json
generated
38
webui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
25
webui/src/app/components/ThemeToggleButton.js
Normal file
25
webui/src/app/components/ThemeToggleButton.js
Normal 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>
|
||||
);
|
||||
}
|
||||
18
webui/src/app/components/theme/MuiThemeProvider.js
Normal file
18
webui/src/app/components/theme/MuiThemeProvider.js
Normal 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>
|
||||
);
|
||||
}
|
||||
27
webui/src/app/components/theme/ThemeContext.js
Normal file
27
webui/src/app/components/theme/ThemeContext.js
Normal 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;
|
||||
}
|
||||
279
webui/src/app/components/theme/darkTheme.js
Normal file
279
webui/src/app/components/theme/darkTheme.js
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
279
webui/src/app/components/theme/lightTheme.js
Normal file
279
webui/src/app/components/theme/lightTheme.js
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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 (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
<body className={inter.className}>
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeContextProvider>
|
||||
<MuiThemeProvider>
|
||||
{children}
|
||||
</MuiThemeProvider>
|
||||
</ThemeContextProvider>
|
||||
</AppRouterCacheProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="container">
|
||||
<header>
|
||||
<h1>🛍️ Stripe Payment Demo</h1>
|
||||
<p>Select a product to purchase</p>
|
||||
</header>
|
||||
<>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Modern App
|
||||
</Typography>
|
||||
<ThemeToggleButton />
|
||||
<Button color="inherit" sx={{ ml: 2 }}>Login</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{!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>
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="h1" gutterBottom>
|
||||
Welcome to Modern UI
|
||||
</Typography>
|
||||
<Typography variant="h5" color="text.secondary" sx={{ mb: 4 }}>
|
||||
Experience the beauty of modern Material-UI theming
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<Button variant="contained" size="large">
|
||||
Get Started
|
||||
</Button>
|
||||
<Button variant="outlined" size="large">
|
||||
Learn More
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{clientSecret && (
|
||||
<Elements options={options} stripe={stripePromise}>
|
||||
<PaymentForm
|
||||
onSuccess={handlePaymentSuccess}
|
||||
orderId={orderId}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<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>
|
||||
|
||||
<button
|
||||
onClick={handleBackToProducts}
|
||||
className="back-btn"
|
||||
>
|
||||
← Back to Products
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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
|
||||
/>
|
||||
<TextField
|
||||
label="Email Address"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Message"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
/>
|
||||
<Button variant="contained" size="large">
|
||||
Send Message
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user