First Commit
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "oxc"],
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the Oxlint configuration
|
||||
|
||||
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "oxc"],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Mani Candles - Premium hand-poured soy and coconut candles crafted in India. Explore luxury scents, custom candle customizers, B2B wholesale, and wedding gifts." />
|
||||
<!-- Google Fonts: Inter and Cormorant Garamond -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Great+Vibes&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<title>Mani Candles | Luxury Handmade Soy Candles India</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "oxlint",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^9.2.0",
|
||||
"@mui/material": "^9.2.0",
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
"axios": "^1.18.1",
|
||||
"framer-motion": "^12.42.2",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-helmet-async": "^3.0.0",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"swiper": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"oxlint": "^1.71.0",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.1.1"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
Deprecated template file.
|
||||
Global styles have been migrated to src/index.css, and Material UI is utilized for theme design variables.
|
||||
*/
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Deprecated template file.
|
||||
// The main application wrapper and routing configuration has been relocated to src/app/App.tsx
|
||||
// in accordance with feature-based architecture guidelines.
|
||||
export default {};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import React, { useState } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import theme from '../theme/theme';
|
||||
import { CartProvider } from '../contexts/CartContext';
|
||||
import { WishlistProvider } from '../contexts/WishlistContext';
|
||||
import AppRoutes from '../routes/AppRoutes';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const App: React.FC = () => {
|
||||
// Global wholesale vs retail context state
|
||||
const [isB2BMode, setIsB2BMode] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem('mani_candles_b2b_mode');
|
||||
return saved === 'true';
|
||||
});
|
||||
|
||||
const handleSetB2BMode = (val: boolean) => {
|
||||
localStorage.setItem('mani_candles_b2b_mode', val ? 'true' : 'false');
|
||||
setIsB2BMode(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CartProvider>
|
||||
<WishlistProvider>
|
||||
<BrowserRouter>
|
||||
<AppRoutes isB2BMode={isB2BMode} setIsB2BMode={handleSetB2BMode} />
|
||||
</BrowserRouter>
|
||||
</WishlistProvider>
|
||||
</CartProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</HelmetProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
const messages = [
|
||||
'Free complimentary shipping across India on B2C orders above ₹999',
|
||||
'Corporate Gifting & Wedding return favors: Custom branding & wholesale prices available',
|
||||
'Design your own olfactory signature with our Custom Candle Builder',
|
||||
'Sustainable luxury: 100% natural hand-poured soy wax candles'
|
||||
];
|
||||
|
||||
export const AnnouncementBar: React.FC = () => {
|
||||
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
|
||||
const [fade, setFade] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const rotateInterval = setInterval(() => {
|
||||
setFade(false);
|
||||
setTimeout(() => {
|
||||
setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % messages.length);
|
||||
setFade(true);
|
||||
}, 500); // match transition duration
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(rotateInterval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'secondary.main',
|
||||
color: '#F5EFEB',
|
||||
py: 0.8,
|
||||
px: 2,
|
||||
textAlign: 'center',
|
||||
position: 'relative',
|
||||
zIndex: 1201,
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
letterSpacing: '0.08em',
|
||||
fontWeight: 400,
|
||||
textTransform: 'uppercase',
|
||||
opacity: fade ? 1 : 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{messages[currentMessageIndex]}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementBar;
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { Breadcrumbs as MuiBreadcrumbs, Link, Typography, Container, Box } from '@mui/material';
|
||||
import { Link as RouterLink, useLocation } from 'react-router-dom';
|
||||
import { IoIosArrowForward } from 'react-icons/io';
|
||||
|
||||
export const Breadcrumbs: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const pathnames = location.pathname.split('/').filter((x) => x);
|
||||
|
||||
// Don't show breadcrumbs on homepage
|
||||
if (pathnames.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 2, backgroundColor: 'background.default', borderBottom: '1px solid rgba(230, 223, 213, 0.4)' }}>
|
||||
<Container maxWidth="lg">
|
||||
<MuiBreadcrumbs
|
||||
separator={<IoIosArrowForward size={10} style={{ color: '#8E7557' }} />}
|
||||
aria-label="breadcrumb"
|
||||
>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
underline="hover"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.8rem', letterSpacing: '0.05em', '&:hover': { color: 'primary.main' } }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
{pathnames.map((value, index) => {
|
||||
const last = index === pathnames.length - 1;
|
||||
const to = `/${pathnames.slice(0, index + 1).join('/')}`;
|
||||
const displayName = value
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase()); // Capitalize first letters
|
||||
|
||||
return last ? (
|
||||
<Typography
|
||||
key={to}
|
||||
color="text.primary"
|
||||
sx={{ fontSize: '0.8rem', letterSpacing: '0.05em', fontWeight: 500 }}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
) : (
|
||||
<Link
|
||||
key={to}
|
||||
component={RouterLink}
|
||||
to={to}
|
||||
underline="hover"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: '0.8rem', letterSpacing: '0.05em', '&:hover': { color: 'primary.main' } }}
|
||||
>
|
||||
{displayName}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MuiBreadcrumbs>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Drawer,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
Avatar,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Button,
|
||||
Divider,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import { MdClose, MdOutlineDeleteOutline, MdAdd, MdRemove } from 'react-icons/md';
|
||||
import { MdOutlineShoppingBag } from 'react-icons/md';
|
||||
import { useCart } from '../../contexts/CartContext';
|
||||
import { formatCurrency } from '../../utils/helpers';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const CartDrawer: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { cartItems, isCartOpen, setIsCartOpen, removeFromCart, updateQuantity, cartTotal, clearCart } = useCart();
|
||||
const [checkoutSuccess, setCheckoutSuccess] = useState(false);
|
||||
|
||||
const handleCheckout = () => {
|
||||
// Simulate checkout action
|
||||
setCheckoutSuccess(true);
|
||||
};
|
||||
|
||||
const handleFinishCheckout = () => {
|
||||
clearCart();
|
||||
setCheckoutSuccess(false);
|
||||
setIsCartOpen(false);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={isCartOpen}
|
||||
onClose={() => setIsCartOpen(false)}
|
||||
disableScrollLock
|
||||
slotProps={{ paper: { sx: {
|
||||
width: { xs: '100%', sm: 440 },
|
||||
backgroundColor: 'background.default',
|
||||
} } }}
|
||||
>
|
||||
{/* Drawer Header */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 3, borderBottom: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h6" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 600, letterSpacing: '0.05em' }}>
|
||||
Shopping Cart ({cartItems.length})
|
||||
</Typography>
|
||||
<IconButton onClick={() => setIsCartOpen(false)}>
|
||||
<MdClose size={24} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Drawer Content */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', p: 3 }}>
|
||||
{cartItems.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', textAlign: 'center', py: 8 }}>
|
||||
<MdOutlineShoppingBag size={48} style={{ color: '#C5A880', marginBottom: 16 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 500, mb: 1 }}>
|
||||
Your cart is empty
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 4, px: 4, lineHeight: 1.6 }}>
|
||||
Add items to your cart from our shop or design a personalized blend in the custom workspace.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setIsCartOpen(false);
|
||||
navigate('/shop');
|
||||
}}
|
||||
sx={{ px: 4 }}
|
||||
>
|
||||
Go to Shop
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{cartItems.map((item, index) => {
|
||||
const isB2B = item.mode === 'B2B';
|
||||
const unitPrice = isB2B ? item.product.b2bPrice : item.product.price;
|
||||
return (
|
||||
<React.Fragment key={`${item.product.id}-${item.mode}-${index}`}>
|
||||
<ListItem alignItems="flex-start" sx={{ px: 0, py: 2.5 }}>
|
||||
<ListItemAvatar sx={{ mr: 2 }}>
|
||||
<Avatar
|
||||
src={item.product.images[0]}
|
||||
variant="rounded"
|
||||
sx={{ width: 80, height: 80, border: '1px solid #E6DFD5' }}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', mb: 0.5 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{item.product.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={isB2B ? 'Wholesale (B2B)' : 'Retail'}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color={isB2B ? 'primary' : 'default'}
|
||||
sx={{ height: 18, fontSize: '0.65rem', borderRadius: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
{/* Render customizations if custom candle */}
|
||||
{item.customizations && (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'rgba(142, 117, 87, 0.04)',
|
||||
p: 1,
|
||||
borderRadius: 1.5,
|
||||
border: '1px dashed #C5A880',
|
||||
my: 1,
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
<strong>Jar:</strong> {item.customizations.jar} | <strong>Wax:</strong> {item.customizations.wax}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
<strong>Blend:</strong> {item.customizations.scentBlend}
|
||||
</Typography>
|
||||
{item.customizations.labelText && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
<strong>Label Text:</strong> "{item.customizations.labelText}" ({item.customizations.labelFont})
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Typography variant="body2" color="primary" sx={{ fontWeight: 500, mb: 1.5 }}>
|
||||
{formatCurrency(unitPrice)} each
|
||||
</Typography>
|
||||
|
||||
{/* Quantity Selector */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', border: '1px solid #E6DFD5', borderRadius: '50px', width: 90, px: 0.5 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => updateQuantity(item.product.id, item.quantity - 1, item.mode)}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<MdRemove size={14} />
|
||||
</IconButton>
|
||||
<Typography variant="body2" sx={{ flex: 1, textAlign: 'center', fontWeight: 600 }}>
|
||||
{item.quantity}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => updateQuantity(item.product.id, item.quantity + 1, item.mode)}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<MdAdd size={14} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction sx={{ top: 20 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => removeFromCart(item.product.id, item.mode)}
|
||||
sx={{ color: 'text.secondary', '&:hover': { color: 'error.main' } }}
|
||||
>
|
||||
<MdOutlineDeleteOutline size={20} />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<Divider sx={{ borderColor: 'rgba(230, 223, 213, 0.5)' }} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Drawer Footer Summary */}
|
||||
{cartItems.length > 0 && (
|
||||
<Box sx={{ p: 3, borderTop: '1px solid #E6DFD5', backgroundColor: '#FFFDFB' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
Subtotal
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="primary" sx={{ fontWeight: 700 }}>
|
||||
{formatCurrency(cartTotal)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 3 }}>
|
||||
* Shipping & taxes calculated at checkout. GST invoice option available for B2B orders.
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCheckout}
|
||||
sx={{ py: 1.5, mb: 1.5, fontSize: '0.85rem', letterSpacing: '0.1em' }}
|
||||
>
|
||||
Proceed to Checkout
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="text"
|
||||
onClick={() => setIsCartOpen(false)}
|
||||
sx={{ color: 'text.secondary', fontSize: '0.8rem' }}
|
||||
>
|
||||
Continue Shopping
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
{/* Checkout Simulation Dialog */}
|
||||
<Dialog open={checkoutSuccess} onClose={handleFinishCheckout} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, fontSize: '1.6rem', textAlign: 'center', pt: 4 }}>
|
||||
Order Initiated
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ textAlign: 'center', px: 4 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6, mb: 2 }}>
|
||||
Thank you for shopping with Mani Candles!
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, color: 'primary.main', mb: 3 }}>
|
||||
{cartItems.some(item => item.mode === 'B2B')
|
||||
? 'Our corporate B2B sales desk is generating a custom GST invoice. We will reach out to you within 2 business hours.'
|
||||
: 'Your secure B2C payment portal has been verified. A tracking link is dispatched to your registered email.'}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
Estimated Delivery: 3-5 Business Days across India.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: 'center', pb: 4 }}>
|
||||
<Button variant="contained" color="primary" onClick={handleFinishCheckout} sx={{ px: 4 }}>
|
||||
Return to Store
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartDrawer;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography, Button, Slide } from '@mui/material';
|
||||
|
||||
export const CookieConsent: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const consent = localStorage.getItem('mani_candles_cookie_consent');
|
||||
if (!consent) {
|
||||
// Show consent after a short delay
|
||||
const timer = setTimeout(() => setOpen(true), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAccept = () => {
|
||||
localStorage.setItem('mani_candles_cookie_consent', 'accepted');
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Slide direction="up" in={open} mountOnEnter unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
maxWidth: { xs: 'calc(100% - 48px)', sm: 480 },
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid #E6DFD5',
|
||||
borderRadius: 3,
|
||||
boxShadow: '0 8px 30px rgba(0,0,0,0.08)',
|
||||
p: 3,
|
||||
zIndex: 2000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', lineHeight: 1.5 }}>
|
||||
We use cookies to elevate your sensory browsing experience, analyze site traffic, and optimize our B2B and B2C services. By accepting, you consent to our privacy terms.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1.5 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
sx={{ color: 'text.secondary', fontSize: '0.75rem', letterSpacing: '0.05em' }}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ fontSize: '0.75rem', py: 0.8, px: 2 }}
|
||||
onClick={handleAccept}
|
||||
>
|
||||
Accept & Burn
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Slide>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieConsent;
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Container, Grid, Typography, Link, TextField, Button, Divider, IconButton } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { IoLogoInstagram, IoLogoFacebook, IoLogoPinterest, IoLogoTwitter } from 'react-icons/io';
|
||||
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
setSubscribed(true);
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'secondary.main',
|
||||
color: '#F5EFEB',
|
||||
pt: 8,
|
||||
pb: 4,
|
||||
mt: 'auto',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Grid container spacing={4} sx={{ mb: 6 }}>
|
||||
{/* Brand Info */}
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2.5 }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/logo.jpg"
|
||||
alt="Mani Candles Logo"
|
||||
sx={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: 'Cormorant Garamond',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
color: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
MANI CANDLES
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ opacity: 0.8, lineHeight: 1.7, mb: 3, pr: { md: 4 } }}>
|
||||
Artisanal hand-poured luxury candles crafted in small batches in India. Utilizing 100% natural soy and coconut wax blends, therapeutic grade botanical extracts, and lead-free wicks to illuminate your sanctuary cleanly and beautifully.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<IconButton color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1, color: 'primary.light' } }}>
|
||||
<IoLogoInstagram size={20} />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1, color: 'primary.light' } }}>
|
||||
<IoLogoFacebook size={20} />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1, color: 'primary.light' } }}>
|
||||
<IoLogoPinterest size={20} />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1, color: 'primary.light' } }}>
|
||||
<IoLogoTwitter size={20} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Quick Links (B2C) */}
|
||||
<Grid size={{ xs: 6, sm: 3, md: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ letterSpacing: '0.1em', fontWeight: 600, mb: 2.5, color: 'primary.light' }}>
|
||||
SHOP
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Link component={RouterLink} to="/shop" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Shop All
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/shop?category=classic" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Classic Line
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/shop?category=luxury" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Luxury Collection
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/shop?category=seasonal" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Seasonal Scents
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/custom-candle" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Custom Builder
|
||||
</Link>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Wholesaler & B2B Links */}
|
||||
<Grid size={{ xs: 6, sm: 3, md: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ letterSpacing: '0.1em', fontWeight: 600, mb: 2.5, color: 'primary.light' }}>
|
||||
B2B SERVICES
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Link component={RouterLink} to="/b2b/corporate-gifting" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Corporate Gifting
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/b2b/hotels-hospitality" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Hotels & Resorts
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/b2b/private-label" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Private Label / OEM
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/contact" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Custom Manufacture
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/contact" color="inherit" underline="hover" sx={{ fontSize: '0.85rem', opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
Bulk Inquiries
|
||||
</Link>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Newsletter Input */}
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Typography variant="subtitle2" sx={{ letterSpacing: '0.1em', fontWeight: 600, mb: 2.5, color: 'primary.light' }}>
|
||||
THE CIRCLE JOURNAL
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.8, mb: 2.5, lineHeight: 1.6 }}>
|
||||
Receive exclusive alerts on seasonal pourings, brand collaborations, slow-living guides, and special discount codes.
|
||||
</Typography>
|
||||
|
||||
{subscribed ? (
|
||||
<Typography variant="body2" sx={{ color: 'primary.light', fontWeight: 600 }}>
|
||||
Thank you for subscribing. Welcome to the circle.
|
||||
</Typography>
|
||||
) : (
|
||||
<form onSubmit={handleSubscribe}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
placeholder="Enter email address"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: 1,
|
||||
color: '#F5EFEB',
|
||||
'& fieldset': { borderColor: 'rgba(255, 255, 255, 0.15)' },
|
||||
'&:hover fieldset': { borderColor: 'primary.light' },
|
||||
'&.Mui-focused fieldset': { borderColor: 'primary.light' },
|
||||
},
|
||||
'& input::placeholder': { color: '#F5EFEB', opacity: 0.5 },
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
px: 3,
|
||||
borderRadius: 1,
|
||||
textTransform: 'none',
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ borderColor: 'rgba(255, 255, 255, 0.1)', mb: 4 }} />
|
||||
|
||||
{/* Footer Bottom */}
|
||||
<Grid container spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Typography variant="caption" sx={{ opacity: 0.6, fontSize: '0.75rem' }}>
|
||||
© {new Date().getFullYear()} Mani Candles Private Limited. All rights reserved. Made in India.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }} sx={{ display: 'flex', gap: 2.5, justifyContent: { xs: 'flex-start', sm: 'flex-end' } }}>
|
||||
<Link component={RouterLink} to="/privacy-policy" color="inherit" underline="hover" sx={{ fontSize: '0.75rem', opacity: 0.6, '&:hover': { opacity: 1 } }}>
|
||||
Privacy
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/terms-conditions" color="inherit" underline="hover" sx={{ fontSize: '0.75rem', opacity: 0.6, '&:hover': { opacity: 1 } }}>
|
||||
Terms
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/shipping-policy" color="inherit" underline="hover" sx={{ fontSize: '0.75rem', opacity: 0.6, '&:hover': { opacity: 1 } }}>
|
||||
Shipping
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/refund-policy" color="inherit" underline="hover" sx={{ fontSize: '0.75rem', opacity: 0.6, '&:hover': { opacity: 1 } }}>
|
||||
Refunds
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Box,
|
||||
IconButton,
|
||||
Badge,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Drawer,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Container,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||
import { IoIosHeartEmpty, IoIosSearch, IoIosMenu, IoIosClose } from 'react-icons/io';
|
||||
import { MdOutlineShoppingBag } from 'react-icons/md';
|
||||
import { useCart } from '../../contexts/CartContext';
|
||||
import { useWishlist } from '../../contexts/WishlistContext';
|
||||
|
||||
export const Header: React.FC<{ isB2BMode: boolean; setIsB2BMode: (val: boolean) => void }> = ({
|
||||
isB2BMode,
|
||||
setIsB2BMode,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { cartCount, setIsCartOpen } = useCart();
|
||||
const { wishlistItems, setIsWishlistOpen } = useWishlist();
|
||||
|
||||
// Mobile navigation state
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
// Search overlay state
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Dropdown states
|
||||
const [shopAnchor, setShopAnchor] = useState<null | HTMLElement>(null);
|
||||
const [b2bAnchor, setB2BAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleSearchSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (searchQuery.trim()) {
|
||||
navigate(`/shop?search=${encodeURIComponent(searchQuery)}`);
|
||||
setSearchQuery('');
|
||||
setSearchOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar position="sticky" className="glass-header" sx={{ zIndex: 1200 }}>
|
||||
<Container maxWidth="xl">
|
||||
<Toolbar disableGutters sx={{ justifyContent: 'space-between', height: 80 }}>
|
||||
{/* Mobile Burger Menu */}
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }}
|
||||
>
|
||||
<IoIosMenu size={24} />
|
||||
</IconButton>
|
||||
|
||||
{/* Brand Logo */}
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
textDecoration: 'none',
|
||||
flexGrow: { xs: 1, md: 0 },
|
||||
justifyContent: { xs: 'center', md: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src="/logo.jpg"
|
||||
alt="Mani Candles Logo"
|
||||
sx={{
|
||||
height: { xs: 40, md: 50 },
|
||||
width: { xs: 40, md: 50 },
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
border: '1.5px solid #EFE9F3',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontFamily: `'Cormorant Garamond', serif`,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
color: 'secondary.main',
|
||||
fontSize: { xs: '1.1rem', md: '1.4rem' },
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
}}
|
||||
>
|
||||
MANI CANDLES
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{/* Shop Link with dropdown */}
|
||||
<Button
|
||||
color="inherit"
|
||||
onClick={(e) => setShopAnchor(e.currentTarget)}
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 500,
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
Shop
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={shopAnchor}
|
||||
open={Boolean(shopAnchor)}
|
||||
onClose={() => setShopAnchor(null)}
|
||||
disableScrollLock
|
||||
slotProps={{ paper: { sx: {
|
||||
mt: 1,
|
||||
boxShadow: '0 8px 30px rgba(0,0,0,0.06)',
|
||||
border: '1px solid #E6DFD5',
|
||||
borderRadius: 2,
|
||||
p: 1,
|
||||
} } }}
|
||||
>
|
||||
<MenuItem onClick={() => { setShopAnchor(null); navigate('/shop'); }}>Shop All</MenuItem>
|
||||
<MenuItem onClick={() => { setShopAnchor(null); navigate('/shop?category=classic'); }}>Classic Collection</MenuItem>
|
||||
<MenuItem onClick={() => { setShopAnchor(null); navigate('/shop?category=luxury'); }}>Luxury Collection</MenuItem>
|
||||
<MenuItem onClick={() => { setShopAnchor(null); navigate('/shop?category=seasonal'); }}>Seasonal Line</MenuItem>
|
||||
<MenuItem onClick={() => { setShopAnchor(null); navigate('/shop?category=giftbox'); }}>Heritage Gift Boxes</MenuItem>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<MenuItem onClick={() => { setShopAnchor(null); navigate('/custom-candle'); }} sx={{ color: 'primary.main', fontWeight: 600 }}>Custom Candle Builder</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* B2B Services with dropdown */}
|
||||
<Button
|
||||
color="inherit"
|
||||
onClick={(e) => setB2BAnchor(e.currentTarget)}
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 500,
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
B2B & Custom
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={b2bAnchor}
|
||||
open={Boolean(b2bAnchor)}
|
||||
onClose={() => setB2BAnchor(null)}
|
||||
disableScrollLock
|
||||
slotProps={{ paper: { sx: {
|
||||
mt: 1,
|
||||
boxShadow: '0 8px 30px rgba(0,0,0,0.06)',
|
||||
border: '1px solid #E6DFD5',
|
||||
borderRadius: 2,
|
||||
p: 1,
|
||||
} } }}
|
||||
>
|
||||
<MenuItem onClick={() => { setB2BAnchor(null); navigate('/b2b/corporate-gifting'); }}>Corporate Gifting</MenuItem>
|
||||
<MenuItem onClick={() => { setB2BAnchor(null); navigate('/b2b/hotels-hospitality'); }}>Hotels & Resorts</MenuItem>
|
||||
<MenuItem onClick={() => { setB2BAnchor(null); navigate('/b2b/private-label'); }}>Private Label & OEM</MenuItem>
|
||||
<MenuItem onClick={() => { setB2BAnchor(null); navigate('/custom-candle'); }}>Custom Candle Orders</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/journal"
|
||||
color="inherit"
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 500,
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
Journal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/about"
|
||||
color="inherit"
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 500,
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
Our Story
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/contact"
|
||||
color="inherit"
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 500,
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
Contact
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Action Bar: B2C/B2B Switch, Search, Wishlist, Cart */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, md: 2.5 } }}>
|
||||
{/* B2B Toggler */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isB2BMode}
|
||||
onChange={(e) => setIsB2BMode(e.target.checked)}
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
color: isB2BMode ? 'primary.main' : 'text.secondary',
|
||||
display: { xs: 'none', sm: 'inline-block' },
|
||||
}}
|
||||
>
|
||||
{isB2BMode ? 'Wholesale (B2B)' : 'Retail (B2C)'}
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mr: 0 }}
|
||||
/>
|
||||
|
||||
<IconButton onClick={() => setSearchOpen(!searchOpen)} color="inherit">
|
||||
<IoIosSearch size={22} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => setIsWishlistOpen(true)} color="inherit">
|
||||
<Badge badgeContent={wishlistItems.length} color="primary" sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}>
|
||||
<IoIosHeartEmpty size={22} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => setIsCartOpen(true)} color="inherit">
|
||||
<Badge badgeContent={cartCount} color="primary" sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}>
|
||||
<MdOutlineShoppingBag size={22} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
|
||||
{/* Search Overlay Dropdown */}
|
||||
{searchOpen && (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'background.default',
|
||||
borderBottom: '1px solid #E6DFD5',
|
||||
py: 2.5,
|
||||
animation: 'fadeIn 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<form onSubmit={handleSearchSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Search our luxury candle catalog (e.g. Saffron, Sandalwood, Oud)..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
variant="outlined"
|
||||
autoFocus
|
||||
slotProps={{ input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IoIosSearch size={20} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size="small" onClick={() => setSearchOpen(false)}>
|
||||
<IoIosClose size={24} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
sx: {
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#FFFFFF',
|
||||
'& fieldset': { borderColor: '#E6DFD5' },
|
||||
'&:hover fieldset': { borderColor: '#C5A880' },
|
||||
},
|
||||
} }}
|
||||
/>
|
||||
</form>
|
||||
</Container>
|
||||
</Box>
|
||||
)}
|
||||
</AppBar>
|
||||
|
||||
{/* Mobile Menu Drawer */}
|
||||
<Drawer
|
||||
anchor="left"
|
||||
open={mobileOpen}
|
||||
onClose={() => setMobileOpen(false)}
|
||||
disableScrollLock
|
||||
slotProps={{ paper: { sx: { width: 280, backgroundColor: 'background.default', p: 2 } } }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, letterSpacing: '0.1em' }}>
|
||||
NAVIGATION
|
||||
</Typography>
|
||||
<IconButton onClick={() => setMobileOpen(false)}>
|
||||
<IoIosClose size={24} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<List disablePadding>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/shop');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.9rem", letterSpacing: "0.05em" }}>Shop All Candles</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/custom-candle');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.9rem", color: "#8E7557", fontWeight: 600, letterSpacing: "0.05em" }}>Custom Candle Builder</Typography>}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Divider sx={{ my: 1.5 }} />
|
||||
<Typography variant="overline" color="text.secondary" sx={{ pl: 2, letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
B2B Solutions
|
||||
</Typography>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/b2b/corporate-gifting');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.85rem" }}>Corporate Gifting</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/b2b/hotels-hospitality');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.85rem" }}>Hotels & Hospitality</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/b2b/private-label');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.85rem" }}>Private Label & OEM</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Divider sx={{ my: 1.5 }} />
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/journal');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.9rem" }}>The Journal</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/about');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.9rem" }}>Our Story</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
navigate('/contact');
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={<Typography sx={{ fontSize: "0.9rem" }}>Contact Us</Typography>} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, Box, Typography, TextField, Button, IconButton } from '@mui/material';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
|
||||
export const NewsletterPopup: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const dismissed = sessionStorage.getItem('mani_candles_newsletter_dismissed');
|
||||
if (!dismissed) {
|
||||
// Show popup after 8 seconds of page browsing
|
||||
const timer = setTimeout(() => {
|
||||
setOpen(true);
|
||||
}, 8000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
sessionStorage.setItem('mani_candles_newsletter_dismissed', 'true');
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
setSubscribed(true);
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
slotProps={{ paper: { sx: {
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'background.default',
|
||||
border: '1px solid #E6DFD5',
|
||||
} } }}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
color: 'text.secondary',
|
||||
zIndex: 1,
|
||||
'&:hover': { color: 'text.primary' },
|
||||
}}
|
||||
>
|
||||
<MdClose size={22} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
minHeight: 380,
|
||||
}}
|
||||
>
|
||||
{/* Image Side */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundImage: 'url("https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=400")',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
display: { xs: 'none', md: 'block' },
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Form Side */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1.2,
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
The Journal & Circle
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ my: 1.5, color: 'secondary.main', fontWeight: 500 }}>
|
||||
Join Mani Circle
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3, lineHeight: 1.6 }}>
|
||||
Subscribe to unlock 10% off your first order, receive private collection previews, and download our corporate bulk catalogs.
|
||||
</Typography>
|
||||
|
||||
{subscribed ? (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Typography variant="subtitle2" color="primary" sx={{ letterSpacing: '0.05em', fontWeight: 600 }}>
|
||||
Welcome to the circle.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Your invitation and discount code have been dispatched to your inbox.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<form onSubmit={handleSubscribe}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
mb: 2,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 2,
|
||||
'& fieldset': { borderColor: '#E6DFD5' },
|
||||
'&:hover fieldset': { borderColor: '#C5A880' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ py: 1.2, letterSpacing: '0.1em' }}
|
||||
>
|
||||
Join the Circle
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block', fontSize: '0.7rem' }}>
|
||||
We respect your inbox. Unsubscribe at any time.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsletterPopup;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
|
||||
interface SEOHelmetProps {
|
||||
title: string;
|
||||
description: string;
|
||||
canonicalUrl?: string;
|
||||
ogImage?: string;
|
||||
schemaJson?: object;
|
||||
}
|
||||
|
||||
export const SEOHelmet: React.FC<SEOHelmetProps> = ({
|
||||
title,
|
||||
description,
|
||||
canonicalUrl = window.location.href,
|
||||
ogImage = 'https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=800',
|
||||
schemaJson,
|
||||
}) => {
|
||||
const fullTitle = title.includes('Mani Candles') ? title : `${title} | Mani Candles`;
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
{/* Open Graph / Facebook */}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
|
||||
{/* Twitter */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
|
||||
{/* Schema.org Structured Data */}
|
||||
{schemaJson && (
|
||||
<script type="application/ld+json">
|
||||
{JSON.stringify(schemaJson)}
|
||||
</script>
|
||||
)}
|
||||
</Helmet>
|
||||
);
|
||||
};
|
||||
export default SEOHelmet;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export const ScrollCandle: React.FC = () => {
|
||||
const [candleHeight, setCandleHeight] = useState(65);
|
||||
const [isExtinguished, setIsExtinguished] = useState(false);
|
||||
const [visible, setVisible] = useState(true); // Always visible initially
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const scrollPercent = docHeight > 0 ? scrollTop / docHeight : 0;
|
||||
|
||||
// Show scroll candle only if there is a scrollable area on the page
|
||||
if (docHeight > 30) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
// Height decreases from 65vh to 6vh
|
||||
const newHeight = Math.max(6, 65 - scrollPercent * 59);
|
||||
setCandleHeight(newHeight);
|
||||
|
||||
if (scrollPercent >= 0.96) {
|
||||
setIsExtinguished(true);
|
||||
} else {
|
||||
setIsExtinguished(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
// Trigger initially
|
||||
handleScroll();
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Box className="scroll-candle-wrap" sx={{ opacity: visible ? 1 : 0 }}>
|
||||
{/* Flame wrapper */}
|
||||
<Box
|
||||
className="candle-flame-wrap"
|
||||
sx={{
|
||||
opacity: isExtinguished ? 0 : 1,
|
||||
transform: isExtinguished ? 'scale(0)' : 'scale(1)',
|
||||
transition: 'all 0.5s ease',
|
||||
}}
|
||||
>
|
||||
<Box className="flame-glow" />
|
||||
<Box className="flame-outer" />
|
||||
<Box className="flame-inner" />
|
||||
</Box>
|
||||
|
||||
{/* Smoke rises if extinguished */}
|
||||
{isExtinguished && (
|
||||
<Box className="smoke-wrap">
|
||||
<Box className="smoke-particle" />
|
||||
<Box className="smoke-particle" />
|
||||
<Box className="smoke-particle" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Wick */}
|
||||
<Box className="candle-wick" />
|
||||
|
||||
{/* Wick Collar */}
|
||||
<Box className="candle-wick-holder" />
|
||||
|
||||
{/* Candle Body */}
|
||||
<Box className="candle-body" style={{ height: `${candleHeight}vh` }}>
|
||||
<Box className="wax-drip wax-drip-1" />
|
||||
<Box className="wax-drip wax-drip-2" />
|
||||
</Box>
|
||||
|
||||
{/* Base */}
|
||||
<Box className="candle-base" />
|
||||
|
||||
{/* Label */}
|
||||
<Typography className="candle-scroll-label" sx={{ fontSize: '0.62rem', mt: 0.5 }}>
|
||||
{isExtinguished ? 'Burnt out' : 'Scroll to Burn'}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollCandle;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Fab, Zoom } from '@mui/material';
|
||||
import { IoIosArrowUp } from 'react-icons/io';
|
||||
|
||||
export const ScrollToTop: React.FC = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 300) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Zoom in={visible}>
|
||||
<Fab
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={scrollToTop}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 96, // Sits above the WhatsApp button
|
||||
right: { xs: 32, md: 88 }, // Shifted on desktop to align with WhatsApp button
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'background.paper',
|
||||
color: 'secondary.main',
|
||||
border: '1px solid #E6DFD5',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.05)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'secondary.main',
|
||||
color: 'background.paper',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IoIosArrowUp size={20} />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import { Card, Box, Skeleton, Grid } from '@mui/material';
|
||||
|
||||
export const ProductCardSkeleton: React.FC = () => {
|
||||
return (
|
||||
<Card sx={{ border: '1px solid #E6DFD5', boxShadow: 'none' }}>
|
||||
<Skeleton variant="rectangular" height={260} animation="wave" />
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Skeleton variant="text" width="60%" height={20} animation="wave" />
|
||||
<Skeleton variant="text" width="90%" height={28} sx={{ my: 1 }} animation="wave" />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
|
||||
<Skeleton variant="text" width="30%" height={24} animation="wave" />
|
||||
<Skeleton variant="circular" width={32} height={32} animation="wave" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlogCardSkeleton: React.FC = () => {
|
||||
return (
|
||||
<Card sx={{ border: '1px solid #E6DFD5', boxShadow: 'none' }}>
|
||||
<Skeleton variant="rectangular" height={200} animation="wave" />
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Skeleton variant="text" width="40%" height={16} animation="wave" />
|
||||
<Skeleton variant="text" width="95%" height={32} sx={{ my: 1 }} animation="wave" />
|
||||
<Skeleton variant="text" width="80%" height={20} animation="wave" />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mt: 3, gap: 1.5 }}>
|
||||
<Skeleton variant="circular" width={36} height={36} animation="wave" />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Skeleton variant="text" width="50%" height={16} animation="wave" />
|
||||
<Skeleton variant="text" width="30%" height={14} animation="wave" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProductDetailSkeleton: React.FC = () => {
|
||||
return (
|
||||
<Grid container spacing={4} sx={{ py: 4 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Skeleton variant="rectangular" height={450} sx={{ borderRadius: 2 }} animation="wave" />
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
|
||||
<Skeleton variant="rectangular" width={80} height={80} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton variant="rectangular" width={80} height={80} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
<Skeleton variant="rectangular" width={80} height={80} sx={{ borderRadius: 1 }} animation="wave" />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Skeleton variant="text" width="30%" height={20} animation="wave" />
|
||||
<Skeleton variant="text" width="85%" height={48} sx={{ my: 1 }} animation="wave" />
|
||||
<Skeleton variant="text" width="95%" height={24} animation="wave" />
|
||||
<Skeleton variant="text" width="90%" height={24} animation="wave" />
|
||||
<Skeleton variant="rectangular" height={60} sx={{ my: 3, borderRadius: 1 }} animation="wave" />
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
|
||||
<Skeleton variant="rectangular" width="60%" height={48} sx={{ borderRadius: 6 }} animation="wave" />
|
||||
<Skeleton variant="rectangular" width="35%" height={48} sx={{ borderRadius: 6 }} animation="wave" />
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Fab, Tooltip, Box, Typography } from '@mui/material';
|
||||
import { IoLogoWhatsapp } from 'react-icons/io';
|
||||
|
||||
export const WhatsAppButton: React.FC = () => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const phoneNumber = '919999999999'; // Simulated brand WhatsApp contact
|
||||
const message = encodeURIComponent(
|
||||
'Hi Mani Candles! I am browsing your digital store and would like to inquire about B2C candle delivery or B2B corporate catalogs.'
|
||||
);
|
||||
const whatsappUrl = `https://wa.me/${phoneNumber}?text=${message}`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: { xs: 24, md: 80 }, // Shifted on desktop to make room for the scroll candle
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
{hovered && (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
py: 1,
|
||||
px: 2,
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.06)',
|
||||
border: '1px solid #E6DFD5',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontWeight: 500, color: 'secondary.main', letterSpacing: '0.05em' }}>
|
||||
Inquire on WhatsApp
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Tooltip title="" placement="left">
|
||||
<Fab
|
||||
color="success"
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
backgroundColor: '#25D366',
|
||||
color: '#FFFFFF',
|
||||
boxShadow: '0 4px 20px rgba(37, 211, 102, 0.4)',
|
||||
'&:hover': {
|
||||
backgroundColor: '#128C7E',
|
||||
boxShadow: '0 8px 30px rgba(18, 140, 126, 0.6)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IoLogoWhatsapp size={26} />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatsAppButton;
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Drawer,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
Avatar,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Button,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { MdClose, MdOutlineDeleteOutline } from 'react-icons/md';
|
||||
import { IoIosHeartEmpty } from 'react-icons/io';
|
||||
import { useWishlist } from '../../contexts/WishlistContext';
|
||||
import { useCart } from '../../contexts/CartContext';
|
||||
import { formatCurrency } from '../../utils/helpers';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const WishlistDrawer: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { wishlistItems, isWishlistOpen, setIsWishlistOpen, removeFromWishlist } = useWishlist();
|
||||
const { addToCart } = useCart();
|
||||
|
||||
const handleMoveToCart = (product: any) => {
|
||||
addToCart(product, 1, 'B2C'); // Moves as single item B2C
|
||||
removeFromWishlist(product.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={isWishlistOpen}
|
||||
onClose={() => setIsWishlistOpen(false)}
|
||||
disableScrollLock
|
||||
slotProps={{ paper: { sx: {
|
||||
width: { xs: '100%', sm: 400 },
|
||||
backgroundColor: 'background.default',
|
||||
} } }}
|
||||
>
|
||||
{/* Drawer Header */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 3, borderBottom: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h6" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 600, letterSpacing: '0.05em' }}>
|
||||
Your Wishlist ({wishlistItems.length})
|
||||
</Typography>
|
||||
<IconButton onClick={() => setIsWishlistOpen(false)}>
|
||||
<MdClose size={24} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Drawer Content */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', p: 3 }}>
|
||||
{wishlistItems.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', textAlign: 'center', py: 8 }}>
|
||||
<IoIosHeartEmpty size={48} style={{ color: '#C5A880', marginBottom: 16 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 500, mb: 1 }}>
|
||||
Your wishlist is empty
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 4, px: 4, lineHeight: 1.6 }}>
|
||||
Save your favorite luxury pouring scents here to review and purchase them later.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setIsWishlistOpen(false);
|
||||
navigate('/shop');
|
||||
}}
|
||||
sx={{ px: 4 }}
|
||||
>
|
||||
Explore Products
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{wishlistItems.map((item) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<ListItem alignItems="flex-start" sx={{ px: 0, py: 2 }}>
|
||||
<ListItemAvatar sx={{ mr: 2 }}>
|
||||
<Avatar
|
||||
src={item.images[0]}
|
||||
variant="rounded"
|
||||
sx={{ width: 70, height: 70, border: '1px solid #E6DFD5' }}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5, pr: 4 }}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="body2" color="primary" sx={{ fontWeight: 500, mb: 1 }}>
|
||||
{formatCurrency(item.price)}
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleMoveToCart(item)}
|
||||
sx={{ fontSize: '0.7rem', py: 0.5, px: 1.5 }}
|
||||
>
|
||||
Move To Cart
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction sx={{ top: 20 }}>
|
||||
<IconButton size="small" onClick={() => removeFromWishlist(item.id)} sx={{ color: 'text.secondary', '&:hover': { color: 'error.main' } }}>
|
||||
<MdOutlineDeleteOutline size={20} />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<Divider sx={{ borderColor: 'rgba(230, 223, 213, 0.5)' }} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default WishlistDrawer;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { Product, CartItem } from '../types';
|
||||
|
||||
interface CartContextType {
|
||||
cartItems: CartItem[];
|
||||
addToCart: (product: Product, quantity: number, mode: 'B2C' | 'B2B', customizations?: CartItem['customizations']) => void;
|
||||
removeFromCart: (productId: string, mode: 'B2C' | 'B2B') => void;
|
||||
updateQuantity: (productId: string, quantity: number, mode: 'B2C' | 'B2B') => void;
|
||||
clearCart: () => void;
|
||||
cartTotal: number;
|
||||
cartCount: number;
|
||||
isCartOpen: boolean;
|
||||
setIsCartOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
|
||||
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>(() => {
|
||||
const saved = localStorage.getItem('mani_candles_cart');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('mani_candles_cart', JSON.stringify(cartItems));
|
||||
}, [cartItems]);
|
||||
|
||||
const addToCart = (
|
||||
product: Product,
|
||||
quantity: number,
|
||||
mode: 'B2C' | 'B2B',
|
||||
customizations?: CartItem['customizations']
|
||||
) => {
|
||||
setCartItems((prevItems) => {
|
||||
// Find if item already exists with matching id, mode, and customizations
|
||||
const existingIndex = prevItems.findIndex(
|
||||
(item) =>
|
||||
item.product.id === product.id &&
|
||||
item.mode === mode &&
|
||||
JSON.stringify(item.customizations) === JSON.stringify(customizations)
|
||||
);
|
||||
|
||||
if (existingIndex > -1) {
|
||||
const newItems = [...prevItems];
|
||||
newItems[existingIndex].quantity += quantity;
|
||||
return newItems;
|
||||
}
|
||||
|
||||
return [...prevItems, { product, quantity, mode, customizations }];
|
||||
});
|
||||
setIsCartOpen(true); // Auto-open cart drawer on add
|
||||
};
|
||||
|
||||
const removeFromCart = (productId: string, mode: 'B2C' | 'B2B') => {
|
||||
setCartItems((prevItems) =>
|
||||
prevItems.filter((item) => !(item.product.id === productId && item.mode === mode))
|
||||
);
|
||||
};
|
||||
|
||||
const updateQuantity = (productId: string, quantity: number, mode: 'B2C' | 'B2B') => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(productId, mode);
|
||||
return;
|
||||
}
|
||||
setCartItems((prevItems) =>
|
||||
prevItems.map((item) =>
|
||||
item.product.id === productId && item.mode === mode ? { ...item, quantity } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const clearCart = () => {
|
||||
setCartItems([]);
|
||||
};
|
||||
|
||||
const cartTotal = cartItems.reduce((acc, item) => {
|
||||
const unitPrice = item.mode === 'B2B' ? item.product.b2bPrice : item.product.price;
|
||||
return acc + unitPrice * item.quantity;
|
||||
}, 0);
|
||||
|
||||
const cartCount = cartItems.reduce((acc, item) => acc + item.quantity, 0);
|
||||
|
||||
return (
|
||||
<CartContext.Provider
|
||||
value={{
|
||||
cartItems,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart,
|
||||
cartTotal,
|
||||
cartCount,
|
||||
isCartOpen,
|
||||
setIsCartOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCart = () => {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) {
|
||||
throw new Error('useCart must be used within a CartProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { Product } from '../types';
|
||||
|
||||
interface WishlistContextType {
|
||||
wishlistItems: Product[];
|
||||
addToWishlist: (product: Product) => void;
|
||||
removeFromWishlist: (productId: string) => void;
|
||||
isInWishlist: (productId: string) => boolean;
|
||||
isWishlistOpen: boolean;
|
||||
setIsWishlistOpen: (open: boolean) => void;
|
||||
toggleWishlist: (product: Product) => void;
|
||||
}
|
||||
|
||||
const WishlistContext = createContext<WishlistContextType | undefined>(undefined);
|
||||
|
||||
export const WishlistProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [wishlistItems, setWishlistItems] = useState<Product[]>(() => {
|
||||
const saved = localStorage.getItem('mani_candles_wishlist');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
const [isWishlistOpen, setIsWishlistOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('mani_candles_wishlist', JSON.stringify(wishlistItems));
|
||||
}, [wishlistItems]);
|
||||
|
||||
const addToWishlist = (product: Product) => {
|
||||
setWishlistItems((prev) => {
|
||||
if (prev.some((item) => item.id === product.id)) return prev;
|
||||
return [...prev, product];
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromWishlist = (productId: string) => {
|
||||
setWishlistItems((prev) => prev.filter((item) => item.id !== productId));
|
||||
};
|
||||
|
||||
const isInWishlist = (productId: string) => {
|
||||
return wishlistItems.some((item) => item.id === productId);
|
||||
};
|
||||
|
||||
const toggleWishlist = (product: Product) => {
|
||||
if (isInWishlist(product.id)) {
|
||||
removeFromWishlist(product.id);
|
||||
} else {
|
||||
addToWishlist(product);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<WishlistContext.Provider
|
||||
value={{
|
||||
wishlistItems,
|
||||
addToWishlist,
|
||||
removeFromWishlist,
|
||||
isInWishlist,
|
||||
isWishlistOpen,
|
||||
setIsWishlistOpen,
|
||||
toggleWishlist,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WishlistContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWishlist = () => {
|
||||
const context = useContext(WishlistContext);
|
||||
if (!context) {
|
||||
throw new Error('useWishlist must be used within a WishlistProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import React from 'react';
|
||||
import { Container, Grid, Box, Typography, Paper, Avatar } from '@mui/material';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
|
||||
const TIMELINE = [
|
||||
{ year: '2023', title: 'The Dream in Kochi', desc: 'Mani Nair starts testing candle waxes in his kitchen, seeking a cleaner, soot-free burn using natural Indian spices and extracts.' },
|
||||
{ year: '2024', title: 'Studio Launch', desc: 'Establishing our dedicated small-batch pouring studio in Kochi, Kerala, committing entirely to GMO-free soy wax and organic cotton wicks.' },
|
||||
{ year: '2025', title: 'B2B Hospitality Expansion', desc: 'Partnering with premium heritage hotels and wellness resorts in Southern India to design their signature ambient scent profiles.' },
|
||||
{ year: '2026', title: 'Bespoke Customizer', desc: 'Launching our digital flagship workspace, empowering B2C customers globally to blend custom scent percentages online.' }
|
||||
];
|
||||
|
||||
export const AboutPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Our Story & Craftsmanship | Mani Candles"
|
||||
description="Discover the history, sustainability values, and artisanal hand-pouring processes defining Mani Candles in India."
|
||||
/>
|
||||
|
||||
{/* Brand Hero */}
|
||||
<Box sx={{ py: 10, textAlign: 'center', mb: 8 }}>
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Heritage & Craftsmanship
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 3, color: 'secondary.main', fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Our Story
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1.15rem', lineHeight: 1.8, maxWidth: 700, mx: 'auto' }}>
|
||||
We believe that a candle is not just a source of light, but a medium of presence. Mani Candles was born out of a desire to capture the rich olfactory memory of India in a clean, sustainable, and sophisticated format.
|
||||
</Typography>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{/* The Hand-Poured Process */}
|
||||
<Grid container spacing={6} sx={{ mb: 12, alignItems: 'center' }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 380,
|
||||
borderRadius: 4,
|
||||
backgroundImage: 'url("https://images.unsplash.com/photo-1602872030219-cbf947a44783?auto=format&fit=crop&q=80&w=800")',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
border: '1px solid #E6DFD5',
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
The Studio Ritual
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 3, color: 'secondary.main' }}>
|
||||
Hand-Poured in India
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="p" sx={{ lineHeight: 1.8 }}>
|
||||
Every single candle we produce is mixed and hand-poured in small batches in our Kerala studio. This slow-living dedication ensures that fragrance oils are incorporated evenly at precise thermodynamic cooling states.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.8 }}>
|
||||
From centering lead-free cotton wicks by hand to wrapping finished jars in recycled linen bags, we honor the deliberate timeline of authentic craftsmanship.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Timeline representation */}
|
||||
<Box sx={{ mb: 12 }}>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', textAlign: 'center', mb: 8 }}>
|
||||
Our Journey Timeline
|
||||
</Typography>
|
||||
<Grid container spacing={4}>
|
||||
{TIMELINE.map((item, i) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={i}>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 4,
|
||||
height: '100%',
|
||||
borderRadius: 3,
|
||||
borderColor: '#E6DFD5',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" color="primary" sx={{ fontWeight: 800, mb: 1 }}>
|
||||
{item.year}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'secondary.main', mb: 1.5 }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.8rem', lineHeight: 1.6 }}>
|
||||
{item.desc}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Founder Spot */}
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: { xs: 4, md: 6 },
|
||||
borderRadius: 4,
|
||||
borderColor: '#E6DFD5',
|
||||
backgroundColor: '#FFFDFB',
|
||||
mb: 6,
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={4} sx={{ alignItems: 'center' }}>
|
||||
<Grid size={{ xs: 12, md: 3 }} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Avatar
|
||||
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=200"
|
||||
sx={{ width: 140, height: 140, border: '2px solid #8E7557' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 9 }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
Founder\'s Vision
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 0.5, mb: 2, color: 'secondary.main' }}>
|
||||
"Scent is a Bridge to Memory"
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.8, fontStyle: 'italic', mb: 2 }}>
|
||||
"Growing up in Southern India, scents were central to our festivals and evenings—the damp earth after monsoon rain, mogra garlands, and slow wood-fire smoke. We created Mani Candles to share this rich Indian olfactory vocabulary in its cleanest, most elegant form with the modern world."
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
Mani Nair, Founder & Head Chandler
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutPage;
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Alert,
|
||||
Fade,
|
||||
} from '@mui/material';
|
||||
import type { B2BInquiry } from '../../../types';
|
||||
|
||||
export const B2BInquiryForm: React.FC = () => {
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm<B2BInquiry>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
companyName: '',
|
||||
website: '',
|
||||
businessType: 'corporate',
|
||||
estimatedQuantity: 50,
|
||||
neededBy: '',
|
||||
message: ''
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async (data: B2BInquiry) => {
|
||||
// Simulate API delivery
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
console.log('B2B Inquiry Data Submitted:', data);
|
||||
setSuccess(true);
|
||||
reset();
|
||||
setTimeout(() => setSuccess(false), 8000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: { xs: 3, md: 5 },
|
||||
borderRadius: 3,
|
||||
border: '1px solid #E6DFD5',
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: '0 8px 30px rgba(0,0,0,0.02)',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', mb: 1 }}>
|
||||
Request a Bespoke Proposal
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Complete the fields below and our corporate alchemist desk will compile pricing catalog files and custom renders.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{success && (
|
||||
<Fade in={success}>
|
||||
<Alert severity="success" sx={{ mb: 4, borderRadius: 2 }}>
|
||||
<strong>Inquiry Logged Successfully!</strong> Your corporate contact token has been registered. An alchemist from our team will contact you within 2 business hours.
|
||||
</Alert>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container spacing={3}>
|
||||
{/* Full Name */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: 'Full name is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="Contact Person Name"
|
||||
size="small"
|
||||
error={!!errors.name}
|
||||
helperText={errors.name?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Corporate Email */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Corporate email is required',
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address'
|
||||
}
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="Corporate Email"
|
||||
type="email"
|
||||
size="small"
|
||||
error={!!errors.email}
|
||||
helperText={errors.email?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Phone Number */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="phone"
|
||||
control={control}
|
||||
rules={{ required: 'Phone number is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="Phone / WhatsApp"
|
||||
size="small"
|
||||
error={!!errors.phone}
|
||||
helperText={errors.phone?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Company Name */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="companyName"
|
||||
control={control}
|
||||
rules={{ required: 'Company name is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="Company Name"
|
||||
size="small"
|
||||
error={!!errors.companyName}
|
||||
helperText={errors.companyName?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Website (Optional) */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="website"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="Company Website (Optional)"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Business Type */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="businessType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
select
|
||||
fullWidth
|
||||
label="Industry / Segment"
|
||||
size="small"
|
||||
>
|
||||
<MenuItem value="corporate">Corporate Gifting</MenuItem>
|
||||
<MenuItem value="hospitality">Hotels & Hospitality</MenuItem>
|
||||
<MenuItem value="wedding">Weddings & Event Planners</MenuItem>
|
||||
<MenuItem value="retail">Retail Distribution / Store</MenuItem>
|
||||
<MenuItem value="interior">Interior Designer</MenuItem>
|
||||
<MenuItem value="other">Custom Manufacture / Private Label</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Estimated Quantity */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="estimatedQuantity"
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Quantity is required',
|
||||
min: { value: 20, message: 'Minimum order is 20 units' }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="Estimated Quantity"
|
||||
type="number"
|
||||
size="small"
|
||||
error={!!errors.estimatedQuantity}
|
||||
helperText={errors.estimatedQuantity?.message || 'Minimum B2B order: 20 units'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Needed By Date */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="neededBy"
|
||||
control={control}
|
||||
rules={{ required: 'Target delivery date is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
type="date"
|
||||
label="Target Delivery Date"
|
||||
size="small"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
error={!!errors.neededBy}
|
||||
helperText={errors.neededBy?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Custom Message */}
|
||||
<Grid size={12}>
|
||||
<Controller
|
||||
name="message"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="Please specify custom fragrance blends, jar choices, or packaging logo emboss requirements..."
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Grid size={12}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={isSubmitting}
|
||||
sx={{ py: 1.5, letterSpacing: '0.1em' }}
|
||||
>
|
||||
{isSubmitting ? 'Logging Inquiry...' : 'Submit Bespoke Inquiry'}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default B2BInquiryForm;
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import React from 'react';
|
||||
import { Container, Grid, Box, Typography, Paper, Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||
import { IoIosArrowDown } from 'react-icons/io';
|
||||
import { MdOutlineCardGiftcard, MdOutlineWorkspacePremium, MdSecurity } from 'react-icons/md';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import B2BInquiryForm from '../components/B2BInquiryForm';
|
||||
|
||||
const BENEFITS = [
|
||||
{
|
||||
icon: <MdOutlineCardGiftcard size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Custom Brand Embossing',
|
||||
desc: 'Engrave your corporate brand identity or event message directly onto our ceramic glass vessels and luxury outer slide sleeves.'
|
||||
},
|
||||
{
|
||||
icon: <MdOutlineWorkspacePremium size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Premium Customized Packing',
|
||||
desc: 'Select from textured rigid gift boxes, linen wrapping sleeves, or handwoven canvas bags accompanied by customized copper foil thank-you cards.'
|
||||
},
|
||||
{
|
||||
icon: <MdSecurity size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'GST Invoice & Compliance',
|
||||
desc: 'Receive formal business invoices with dedicated GSTIN registrations across India to secure corporate tax credits easily.'
|
||||
}
|
||||
];
|
||||
|
||||
export const CorporateGiftingPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Corporate Gifting & Bulk Orders | Mani Candles"
|
||||
description="Strengthen client relationships with customized luxury soy candles. Custom logo packaging, GST invoices, and volume discounts available."
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<Box
|
||||
sx={{
|
||||
py: 10,
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#FAF6F0',
|
||||
borderBottom: '1px solid #E6DFD5',
|
||||
mb: 8,
|
||||
backgroundImage: 'linear-gradient(rgba(26, 25, 23, 0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(26, 25, 23, 0.02) 1px, transparent 1px)',
|
||||
backgroundSize: '20px 20px',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Premium B2B Services
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 3, color: 'secondary.main', fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Luxury Corporate Gifting
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1.1rem', lineHeight: 1.8, maxWidth: 700, mx: 'auto' }}>
|
||||
Transform standard corporate tokens into memorable sensory experiences. Discover hand-poured soy candles formulated to project warmth, craftsmanship, and brand excellence.
|
||||
</Typography>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{/* Value Pillars Grid */}
|
||||
<Grid container spacing={4} sx={{ mb: 10 }}>
|
||||
{BENEFITS.map((ben, i) => (
|
||||
<Grid size={{ xs: 12, md: 4 }} key={i}>
|
||||
<Paper variant="outlined" sx={{ p: 4, height: '100%', borderRadius: 3, borderColor: '#E6DFD5', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>{ben.icon}</Box>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
{ben.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
{ben.desc}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Form and Side Info */}
|
||||
<Grid container spacing={6} sx={{ mb: 10 }}>
|
||||
<Grid size={{ xs: 12, md: 5 }} sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
The Custom Standard
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 3, color: 'secondary.main' }}>
|
||||
Designed For Impact
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="p" sx={{ lineHeight: 1.7, mb: 3 }}>
|
||||
Whether rewarding your internal leadership circles, honoring VIP clients, or celebrating Diwali, New Year, or corporate milestones, our candles are calibrated to impress.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.7, mb: 4 }}>
|
||||
We support scaling configurations from 20 units to 5,000+ units, dispatching batch samples to ensure complete satisfaction prior to final line pours.
|
||||
</Typography>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2, borderColor: 'primary.light', backgroundColor: 'rgba(197, 168, 128, 0.04)' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main', mb: 1 }}>
|
||||
India-Wide Delivery Guarantee
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
We handle secure shipping distribution directly to individual client doors or bulk delivery coordinates using temperature-controlled freight logistics.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<B2BInquiryForm />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Corporate FAQs */}
|
||||
<Box sx={{ mt: 10 }}>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', mb: 4, textAlign: 'center' }}>
|
||||
Corporate Gifting FAQ
|
||||
</Typography>
|
||||
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
||||
<Accordion sx={{ borderBottom: '1px solid #E6DFD5', '&:before': { display: 'none' } }} elevation={0}>
|
||||
<AccordionSummary expandIcon={<IoIosArrowDown />}>
|
||||
<Typography sx={{ fontWeight: 600 }}>What is the lead time for corporate custom orders?</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
Standard corporate orders with ready fragrances require 7-10 business days. Custom logo engravings and personalized packaging boxes generally require 14-20 business days from layout sign-off and deposit receipt.
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion sx={{ borderBottom: '1px solid #E6DFD5', '&:before': { display: 'none' } }} elevation={0}>
|
||||
<AccordionSummary expandIcon={<IoIosArrowDown />}>
|
||||
<Typography sx={{ fontWeight: 600 }}>Can we request physical scent samples before ordering?</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
Yes, we offer complimentary tin sampler sets or custom product samples for verified corporate email accounts. Please log your request through the inquiry form with your target requirements.
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CorporateGiftingPage;
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import React from 'react';
|
||||
import { Container, Grid, Box, Typography, Paper, } from '@mui/material';
|
||||
|
||||
import { MdOutlineBed, MdSpa, MdOutlineRestaurant } from 'react-icons/md';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import B2BInquiryForm from '../components/B2BInquiryForm';
|
||||
|
||||
const SERVICES = [
|
||||
{
|
||||
icon: <MdOutlineBed size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Luxury Turn-down Sets',
|
||||
desc: 'Bespoke mini-votive soy candles curated to complement luxury bed turn-down services, presenting guests with relaxing lavender and sandalwood scents.'
|
||||
},
|
||||
{
|
||||
icon: <MdSpa size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Spa & Wellness Olfactories',
|
||||
desc: 'Formulate calming wood and deodar cedar essential oil candles to scent massage therapists blocks, bath retreats, and relaxation zones.'
|
||||
},
|
||||
{
|
||||
icon: <MdOutlineRestaurant size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Ambient Café Candles',
|
||||
desc: 'Odorless or lightly spiced (clove, citrus) clean-burning coconut wax candles to illuminate dinner tables without interfering with food aromas.'
|
||||
}
|
||||
];
|
||||
|
||||
export const HospitalityPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Hotels & Resorts Hospitality Scenting | Mani Candles"
|
||||
description="Signature ambient scenting and guest room turn-down candles. Partner with India's premium handmade soy candle atelier."
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<Box
|
||||
sx={{
|
||||
py: 10,
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#FAF6F0',
|
||||
borderBottom: '1px solid #E6DFD5',
|
||||
mb: 8,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Artisanal Hotel Partners
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 3, color: 'secondary.main', fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Hotels & Resorts Scenting
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1.1rem', lineHeight: 1.8, maxWidth: 700, mx: 'auto' }}>
|
||||
Elevate guest retention through custom-tailored ambient scenting. Establish an exclusive olfactory fingerprint for your resort lobbies, rooms, and wellness centers.
|
||||
</Typography>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{/* Services Grid */}
|
||||
<Grid container spacing={4} sx={{ mb: 10 }}>
|
||||
{SERVICES.map((ser, i) => (
|
||||
<Grid size={{ xs: 12, md: 4 }} key={i}>
|
||||
<Paper variant="outlined" sx={{ p: 4, height: '100%', borderRadius: 3, borderColor: '#E6DFD5', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>{ser.icon}</Box>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
{ser.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
{ser.desc}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={6} sx={{ mb: 10 }}>
|
||||
<Grid size={{ xs: 12, md: 5 }} sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
Scent Branding
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 3, color: 'secondary.main' }}>
|
||||
Olfactory Identity
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="p" sx={{ lineHeight: 1.7 }}>
|
||||
A hotel\'s scent signature builds instant guest recognition. We collaborate with luxury boutique hoteliers to formulate unique signature aromas that reflect the local ecology—whether high Himalayan deodar forest or tropical Kerala coconut and cardamom.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.7, mb: 4 }}>
|
||||
All candles are hand-poured with lead-free cotton or wooden wicks to guarantee soot-free, zero-smoke burning, preserving high indoor air quality standards.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<B2BInquiryForm />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HospitalityPage;
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import React from 'react';
|
||||
import { Container, Grid, Box, Typography, Paper, } from '@mui/material';
|
||||
|
||||
import { MdOutlineScience, MdPalette, MdPrecisionManufacturing } from 'react-icons/md';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import B2BInquiryForm from '../components/B2BInquiryForm';
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
icon: <MdOutlineScience size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Custom Fragrance R&D',
|
||||
desc: 'Work with our master chandlers to extract custom distillations or blend unique synthetic-botanical scent signatures for your cosmetics or lifestyle brand.'
|
||||
},
|
||||
{
|
||||
icon: <MdPalette size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Vessel & Package Sourcing',
|
||||
desc: 'Source bespoke ceramic pots, amber glass vessels, wooden lids, and custom hot-foil printed box packaging from our curated vendor networks in India.'
|
||||
},
|
||||
{
|
||||
icon: <MdPrecisionManufacturing size={32} style={{ color: '#8E7557' }} />,
|
||||
title: 'Small Batch Manufacturing',
|
||||
desc: 'Our studio handles full-scale compounding, hand-pouring, wick centering, label printing, regulatory testing, and logistics packaging.'
|
||||
}
|
||||
];
|
||||
|
||||
export const PrivateLabelPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Private Label Candle Manufacturing & OEM | Mani Candles"
|
||||
description="Launch your luxury candle line. Custom contract manufacturing, custom scent alchemies, and package design support."
|
||||
/>
|
||||
|
||||
{/* Hero Section */}
|
||||
<Box
|
||||
sx={{
|
||||
py: 10,
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#FAF6F0',
|
||||
borderBottom: '1px solid #E6DFD5',
|
||||
mb: 8,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
OEM Contract Manufacturing
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 3, color: 'secondary.main', fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Private Label Candles
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ fontSize: '1.1rem', lineHeight: 1.8, maxWidth: 700, mx: 'auto' }}>
|
||||
Bring your brand fragrance concepts to market. Partner with our studio for full-stack, customized contract manufacturing of luxury soy candles in India.
|
||||
</Typography>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{/* Steps Grid */}
|
||||
<Grid container spacing={4} sx={{ mb: 10 }}>
|
||||
{STEPS.map((step, i) => (
|
||||
<Grid size={{ xs: 12, md: 4 }} key={i}>
|
||||
<Paper variant="outlined" sx={{ p: 4, height: '100%', borderRadius: 3, borderColor: '#E6DFD5', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>{step.icon}</Box>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
{step.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
{step.desc}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={6} sx={{ mb: 10 }}>
|
||||
<Grid size={{ xs: 12, md: 5 }} sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
End-to-End Capabilities
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 3, color: 'secondary.main' }}>
|
||||
Scale Handcrafted Quality
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="p" sx={{ lineHeight: 1.7 }}>
|
||||
Starting a candle line can be challenging due to vessel cracking, wick curling, and fragrance bleeding. Our R&D laboratory performs extensive hot-throw testing, soot tests, and shelf-life reviews, ensuring your candles meet international luxury standards.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.7, mb: 4 }}>
|
||||
We support flexible minimum quantities to help independent boutique brands, beauty salons, and wedding coordinators launch confidently.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<B2BInquiryForm />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivateLabelPage;
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
Button,
|
||||
Alert,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material';
|
||||
import { IoIosArrowDown, IoLogoWhatsapp } from 'react-icons/io';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import { mockFAQs } from '../../../utils/data';
|
||||
|
||||
interface ContactFormData {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const ContactPage: React.FC = () => {
|
||||
const [sent, setSent] = useState(false);
|
||||
const { control, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm<ContactFormData>({
|
||||
defaultValues: { name: '', email: '', subject: '', message: '' }
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ContactFormData) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log('Contact form submitted:', data);
|
||||
setSent(true);
|
||||
reset();
|
||||
setTimeout(() => setSent(false), 5000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Contact Us & Customer Support | Mani Candles"
|
||||
description="Reach out to our customer care team or B2B sales desk. Find our studio coordinates, business hours, and quick support links."
|
||||
/>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{/* Title */}
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Connect With Us
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 2, color: 'secondary.main', fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Get in Touch
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 600, mx: 'auto' }}>
|
||||
Whether inquiring about e-commerce tracking, B2B wholesale prices, or custom corporate gifting proposals.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={5} sx={{ mb: 10 }}>
|
||||
{/* Left Column: Coordinates */}
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Paper variant="outlined" sx={{ p: 4, borderRadius: 3, borderColor: '#E6DFD5', backgroundColor: '#FFFFFF' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'secondary.main', mb: 1.5, letterSpacing: '0.05em' }}>
|
||||
THE STUDIO
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="p" sx={{ lineHeight: 1.6 }}>
|
||||
Mani Candles Private Limited<br />
|
||||
3rd Floor, Aluva Atelier,<br />
|
||||
Kochi, Kerala, India - 683101
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
<strong>Email:</strong> support@manicandles.com
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<strong>Phone:</strong> +91 99999 99999
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 4, borderRadius: 3, borderColor: '#E6DFD5', backgroundColor: '#FFFFFF' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'secondary.main', mb: 1.5, letterSpacing: '0.05em' }}>
|
||||
BUSINESS HOURS
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
Monday to Saturday: 9:00 AM - 6:00 PM IST<br />
|
||||
Closed on Sundays and National Holidays.
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="success"
|
||||
href="https://wa.me/919999999999"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
startIcon={<IoLogoWhatsapp />}
|
||||
sx={{ py: 1.5, borderColor: '#25D366', color: '#25D366', '&:hover': { borderColor: '#128C7E', backgroundColor: 'rgba(37,211,102,0.05)' } }}
|
||||
>
|
||||
Chat on WhatsApp
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Right Column: Contact Form */}
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Paper variant="outlined" sx={{ p: 4, borderRadius: 3, borderColor: '#E6DFD5', backgroundColor: '#FFFFFF' }}>
|
||||
{sent && (
|
||||
<Alert severity="success" sx={{ mb: 3, borderRadius: 2 }}>
|
||||
Your support message has been dispatched. We will reply to your registered email shortly.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: 'Name is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField {...field} fullWidth label="Your Name" size="small" error={!!errors.name} helperText={errors.name?.message} />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Email is required',
|
||||
pattern: { value: /^[S]+@[S]+.[S]+$/i, message: 'Invalid email address' } // Muted simple regex
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField {...field} fullWidth label="Email Address" type="email" size="small" error={!!errors.email} helperText={errors.email?.message} />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Controller
|
||||
name="subject"
|
||||
control={control}
|
||||
rules={{ required: 'Subject is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField {...field} fullWidth label="Subject" size="small" error={!!errors.subject} helperText={errors.subject?.message} />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Controller
|
||||
name="message"
|
||||
control={control}
|
||||
rules={{ required: 'Message content is required' }}
|
||||
render={({ field }) => (
|
||||
<TextField {...field} fullWidth multiline rows={4} label="Message" size="small" error={!!errors.message} helperText={errors.message?.message} />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Button type="submit" variant="contained" color="primary" fullWidth disabled={isSubmitting} sx={{ py: 1.5 }}>
|
||||
{isSubmitting ? 'Sending Message...' : 'Send Message'}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Google Map Mock Frame */}
|
||||
<Box sx={{ mb: 10 }}>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', mb: 3 }}>
|
||||
Studio Location Map
|
||||
</Typography>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
height: 320,
|
||||
backgroundColor: '#ECE7E1',
|
||||
borderRadius: 3,
|
||||
borderColor: '#E6DFD5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundImage: 'radial-gradient(circle, #D8D2C9 10%, transparent 11%)',
|
||||
backgroundSize: '12px 12px',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
|
||||
Google Maps Interactive Frame (Aluva Studio Kochi, Kerala)
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* FAQ Accordions */}
|
||||
<Box sx={{ mt: 10 }}>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', mb: 5, textAlign: 'center' }}>
|
||||
Frequently Asked Questions
|
||||
</Typography>
|
||||
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
||||
{mockFAQs.map((faq, index) => (
|
||||
<Accordion key={index} sx={{ borderBottom: '1px solid #E6DFD5', '&:before': { display: 'none' } }} elevation={0}>
|
||||
<AccordionSummary expandIcon={<IoIosArrowDown />}>
|
||||
<Typography sx={{ fontWeight: 600 }}>{faq.question}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
{faq.answer}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactPage;
|
||||
|
|
@ -0,0 +1,597 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Tabs,
|
||||
Tab,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Slider,
|
||||
TextField,
|
||||
Button,
|
||||
Divider,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
|
||||
import { MdOutlineShoppingBag } from 'react-icons/md';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import { useCart } from '../../../contexts/CartContext';
|
||||
import { formatCurrency } from '../../../utils/helpers';
|
||||
|
||||
// Helper structure for custom builder
|
||||
const JARS = [
|
||||
{ id: 'amber', name: 'Amber Glass', color: 'linear-gradient(135deg, rgba(206,128,52,0.85) 0%, rgba(122,66,13,0.9) 100%)', price: 0, desc: 'Diffuses warm amber glow' },
|
||||
{ id: 'frosted', name: 'Frosted Glass', color: 'linear-gradient(135deg, rgba(255,255,255,0.7) 0%, rgba(220,220,220,0.85) 100%)', price: 50, desc: 'Clean, minimalist aesthetics' },
|
||||
{ id: 'cream', name: 'Ceramic Cream', color: 'linear-gradient(135deg, #FAF6F0 0%, #E6DFD5 100%)', price: 150, desc: 'Earthy, rustic luxury clay' },
|
||||
{ id: 'lavender', name: 'Amethyst Lavender', color: 'linear-gradient(135deg, rgba(202,176,219,0.85) 0%, rgba(118,59,166,0.9) 100%)', price: 100, desc: 'Sophisticated violet aura' },
|
||||
{ id: 'black', name: 'Matte Black Ceramic', color: 'linear-gradient(135deg, #2B2521 0%, #110F0E 100%)', price: 150, desc: 'Opulent, sophisticated styling' },
|
||||
];
|
||||
|
||||
const WAXES = [
|
||||
{ id: 'soy', name: '100% Soy Wax', desc: 'Soot-free clean burn, biodegradable', price: 0 },
|
||||
{ id: 'coconut', name: 'Coconut Wax Blend', desc: 'Superior cold & hot scent throw', price: 100 },
|
||||
{ id: 'beeswax', name: 'Raw Honey Beeswax', desc: 'Natural air purifier, sweet scent', price: 200 },
|
||||
];
|
||||
|
||||
const SCENTS = [
|
||||
{ id: 'sandalwood', name: 'Mysore Sandalwood', category: 'Woody' },
|
||||
{ id: 'jasmine', name: 'Madurai Jasmine', category: 'Floral' },
|
||||
{ id: 'saffron', name: 'Kashmiri Saffron', category: 'Spice' },
|
||||
{ id: 'oud', name: 'Assam Smoky Oud', category: 'Woody' },
|
||||
{ id: 'lime', name: 'Kerala Kaffir Lime', category: 'Citrus' },
|
||||
{ id: 'cardamom', name: 'Malabar Cardamom', category: 'Spice' },
|
||||
];
|
||||
|
||||
interface CustomCandlePageProps {
|
||||
isB2BMode: boolean;
|
||||
}
|
||||
|
||||
export const CustomCandlePage: React.FC<CustomCandlePageProps> = ({ isB2BMode }) => {
|
||||
const { addToCart } = useCart();
|
||||
|
||||
// Customizer options state
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [selectedJar, setSelectedJar] = useState(JARS[0]);
|
||||
const [selectedWax, setSelectedWax] = useState(WAXES[0]);
|
||||
|
||||
// Scents percentages (sums to 100%)
|
||||
const [scentPrimary, setScentPrimary] = useState(SCENTS[0]);
|
||||
const [scentSecondary, setScentSecondary] = useState(SCENTS[1]);
|
||||
const [ratio, setRatio] = useState<number>(60); // Primary Scent ratio
|
||||
const [scentStrength, setScentStrength] = useState<string>('Balanced');
|
||||
|
||||
// Label text and font state
|
||||
const [labelText, setLabelText] = useState('My Sanctuary');
|
||||
const [labelSubText, setLabelSubText] = useState('Hand-Poured Luxury');
|
||||
const [labelFont, setLabelFont] = useState('serif');
|
||||
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
|
||||
// Scent ratio calculation
|
||||
const primaryPct = ratio;
|
||||
const secondaryPct = 100 - ratio;
|
||||
|
||||
// Base pricing
|
||||
const basePrice = isB2BMode ? 750 : 1299;
|
||||
|
||||
// Dynamic Total Price
|
||||
const totalPrice = useMemo(() => {
|
||||
const jarSurcharge = selectedJar.price;
|
||||
const waxSurcharge = selectedWax.price;
|
||||
const strengthSurcharge = scentStrength === 'Intense' ? 100 : scentStrength === 'Subtle' ? -50 : 0;
|
||||
const unitCost = basePrice + jarSurcharge + waxSurcharge + strengthSurcharge;
|
||||
return unitCost * quantity;
|
||||
}, [selectedJar, selectedWax, scentStrength, basePrice, quantity]);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
const customProduct = {
|
||||
id: `custom-${selectedJar.id}-${selectedWax.id}-${scentPrimary.id}-${scentSecondary.id}`,
|
||||
name: `Custom Signature Blend`,
|
||||
tagline: `Personalized blend: ${primaryPct}% ${scentPrimary.name} & ${secondaryPct}% ${scentSecondary.name}`,
|
||||
description: `A custom-blended luxury candle formulated with ${selectedWax.name} in a signature ${selectedJar.name} container. Personalized label reads: "${labelText}".`,
|
||||
details: 'Hand-crafted by request in our Kochi studio.',
|
||||
price: basePrice + selectedJar.price + selectedWax.price + (scentStrength === 'Intense' ? 100 : scentStrength === 'Subtle' ? -50 : 0),
|
||||
b2bPrice: Math.floor((basePrice + selectedJar.price + selectedWax.price) * 0.7),
|
||||
minB2BQuantity: 20,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'classic' as const,
|
||||
fragranceProfile: 'woody' as const,
|
||||
burnTime: 50,
|
||||
waxType: selectedWax.name === '100% Soy Wax' ? ('Soy Wax' as const) : ('Blend' as const),
|
||||
weight: '200g / 7.0 oz',
|
||||
fragranceNotes: {
|
||||
top: [scentPrimary.name],
|
||||
middle: [scentSecondary.name],
|
||||
base: ['Warm Musk']
|
||||
},
|
||||
ingredients: [selectedWax.name, 'Premium Botanical Oils', 'Cotton Wick'],
|
||||
careInstructions: ['Trim wick to 1/4 inch before lighting', 'Burn 2-3 hours on first lighting'],
|
||||
rating: 5.0,
|
||||
reviewsCount: 0,
|
||||
reviews: [],
|
||||
inventory: 99
|
||||
};
|
||||
|
||||
addToCart(customProduct, quantity, isB2BMode ? 'B2B' : 'B2C', {
|
||||
wax: selectedWax.name,
|
||||
jar: selectedJar.name,
|
||||
scentBlend: `${primaryPct}% ${scentPrimary.name} / ${secondaryPct}% ${scentSecondary.name} (${scentStrength} Strength)`,
|
||||
labelText: `${labelText} - ${labelSubText}`,
|
||||
labelFont: labelFont === 'serif' ? 'Cormorant Garamond' : 'Inter'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Custom Candle Builder | Mani Candles"
|
||||
description="Design a personalized signature candle. Choose luxury vessels, select custom scent ratios, and customize labels for weddings or gifts."
|
||||
/>
|
||||
<Container maxWidth="lg">
|
||||
{/* Header Title */}
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Artisanal Workspace
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 2, color: 'secondary.main' }}>
|
||||
Custom Candle Builder
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 650, mx: 'auto', lineHeight: 1.7 }}>
|
||||
Step into the role of head perfumer. Blend your custom olfactory signature, select a premium vessel, and print a custom label to manifest your sanctuary.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{isB2BMode && (
|
||||
<Alert severity="info" sx={{ mb: 4, borderRadius: 2, border: '1px solid #8D4CB8', backgroundColor: 'rgba(90,24,122,0.03)' }}>
|
||||
<strong>Wholesaler Mode Active:</strong> Minimum quantity for custom wholesale candle batches is <strong>20 units</strong>. Perfect for wedding favors, corporate gifting, or hotel branding.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={5}>
|
||||
{/* Left Column: Visual Mock Jar (Interactive Canvas) */}
|
||||
<Grid size={{ xs: 12, md: 5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: { xs: 4, md: 6 },
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.4)',
|
||||
border: '1px solid #E6DFD5',
|
||||
backdropFilter: 'blur(10px)',
|
||||
minHeight: 480,
|
||||
}}
|
||||
>
|
||||
{/* Flame & Wick */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 3,
|
||||
height: 12,
|
||||
backgroundColor: '#3E352F',
|
||||
borderRadius: 1,
|
||||
position: 'relative',
|
||||
mb: -0.5,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{/* Flame Graphic */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 14,
|
||||
height: 24,
|
||||
borderRadius: '50% 50% 50% 50% / 60% 60% 40% 40%',
|
||||
backgroundColor: '#FFB84C',
|
||||
boxShadow: '0 0 10px #FF9F29, 0 0 20px #FF8811',
|
||||
position: 'absolute',
|
||||
top: -24,
|
||||
left: -6,
|
||||
transform: 'scale(1)',
|
||||
animation: 'float 3s ease-in-out infinite, pulse-aura 2s infinite',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Candle Vessel Shape */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 180,
|
||||
height: 230,
|
||||
borderRadius: '12px 12px 20px 20px',
|
||||
background: selectedJar.color,
|
||||
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1)',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
border: selectedJar.id === 'frosted' ? '1px solid rgba(255,255,255,0.5)' : '1px solid rgba(0,0,0,0.1)',
|
||||
transition: 'all 0.5s ease',
|
||||
}}
|
||||
>
|
||||
{/* Wax Inside Overlay (Soy/Beeswax color change) */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '80%',
|
||||
backgroundColor: selectedWax.id === 'beeswax' ? 'rgba(251, 191, 36, 0.4)' : 'rgba(254, 253, 247, 0.65)',
|
||||
borderTop: '2px solid rgba(255,255,255,0.4)',
|
||||
zIndex: 1,
|
||||
transition: 'all 0.5s ease',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Customized Label Sticker */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 130,
|
||||
height: 100,
|
||||
backgroundColor: '#FFFFFF',
|
||||
border: '1px solid #5A187A',
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 1.5,
|
||||
zIndex: 2,
|
||||
boxShadow: '0 4px 10px rgba(0,0,0,0.06)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.55rem',
|
||||
letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'primary.main',
|
||||
fontWeight: 600,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
Mani Signature
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: labelFont === 'serif' ? 'Cormorant Garamond' : 'Inter',
|
||||
fontSize: labelText.length > 15 ? '0.75rem' : '0.95rem',
|
||||
fontWeight: labelFont === 'serif' ? 600 : 500,
|
||||
color: '#1C1917',
|
||||
lineHeight: 1.1,
|
||||
wordBreak: 'break-word',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{labelText || 'Your Label'}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.55rem',
|
||||
color: 'text.secondary',
|
||||
letterSpacing: '0.05em',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
{labelSubText || 'Hand-Poured'}
|
||||
</Typography>
|
||||
<Divider sx={{ width: '30%', borderColor: '#E6DFD5', my: 0.5 }} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '0.45rem',
|
||||
color: '#5A187A',
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{primaryPct}% {scentPrimary.name.split(' ')[1] || scentPrimary.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Specs Label Summary */}
|
||||
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main' }}>
|
||||
{labelText}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.8rem', mt: 0.5 }}>
|
||||
{selectedWax.name} • {selectedJar.name} ({primaryPct}%/{secondaryPct}%)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Right Column: Steps Selector Panel */}
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<Paper variant="outlined" sx={{ p: 4, borderRadius: 3, backgroundColor: '#FFFFFF', border: '1px solid #EFE9F3' }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, val) => setActiveTab(val)}
|
||||
variant="fullWidth"
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
sx={{ borderBottom: '1px solid #EFE9F3', mb: 4 }}
|
||||
>
|
||||
<Tab label="1. Jar & Wax" sx={{ letterSpacing: '0.05em', fontSize: '0.8rem' }} />
|
||||
<Tab label="2. Scent Blend" sx={{ letterSpacing: '0.05em', fontSize: '0.8rem' }} />
|
||||
<Tab label="3. Personalize" sx={{ letterSpacing: '0.05em', fontSize: '0.8rem' }} />
|
||||
</Tabs>
|
||||
|
||||
{/* Tab 1: Vessels & Waxes */}
|
||||
{activeTab === 0 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{/* Jar Vessels */}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend" sx={{ color: 'secondary.main', fontWeight: 600, mb: 2, fontSize: '0.9rem', letterSpacing: '0.05em', textTransform: 'uppercase' }}>
|
||||
Select Vessel Finish
|
||||
</FormLabel>
|
||||
<RadioGroup value={selectedJar.id} onChange={(e) => setSelectedJar(JARS.find(j => j.id === e.target.value) || JARS[0])}>
|
||||
<Grid container spacing={2}>
|
||||
{JARS.map((jar) => (
|
||||
<Grid size={{ xs: 12, sm: 6 }} key={jar.id}>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2.5,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
border: selectedJar.id === jar.id ? '2px solid #5A187A' : '1px solid #EFE9F3',
|
||||
backgroundColor: selectedJar.id === jar.id ? 'rgba(90,24,122,0.03)' : 'transparent',
|
||||
'&:hover': { borderColor: 'primary.light' }
|
||||
}}
|
||||
onClick={() => setSelectedJar(jar)}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Radio value={jar.id} size="small" sx={{ p: 0 }} />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{jar.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{jar.desc} {jar.price > 0 && `(+₹${jar.price})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* Wax Type */}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend" sx={{ color: 'secondary.main', fontWeight: 600, mb: 2, fontSize: '0.9rem', letterSpacing: '0.05em', textTransform: 'uppercase' }}>
|
||||
Select Organic Wax Type
|
||||
</FormLabel>
|
||||
<RadioGroup value={selectedWax.id} onChange={(e) => setSelectedWax(WAXES.find(w => w.id === e.target.value) || WAXES[0])}>
|
||||
<Grid container spacing={2}>
|
||||
{WAXES.map((wax) => (
|
||||
<Grid size={{ xs: 12, sm: 6 }} key={wax.id}>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2.5,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
border: selectedWax.id === wax.id ? '2px solid #5A187A' : '1px solid #EFE9F3',
|
||||
backgroundColor: selectedWax.id === wax.id ? 'rgba(90,24,122,0.03)' : 'transparent',
|
||||
'&:hover': { borderColor: 'primary.light' }
|
||||
}}
|
||||
onClick={() => setSelectedWax(wax)}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Radio value={wax.id} size="small" sx={{ p: 0 }} />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{wax.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{wax.desc} {wax.price > 0 && `(+₹${wax.price})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Scent Blend ratios */}
|
||||
{activeTab === 1 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{/* Select Primary Scent */}
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<FormLabel sx={{ color: 'secondary.main', fontWeight: 600, mb: 1, fontSize: '0.85rem' }}>Primary Fragrance</FormLabel>
|
||||
<TextField
|
||||
select
|
||||
value={scentPrimary.id}
|
||||
onChange={(e) => setScentPrimary(SCENTS.find(s => s.id === e.target.value) || SCENTS[0])}
|
||||
slotProps={{ select: { native: true } }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ '& select': { py: 1.2 } }}
|
||||
>
|
||||
{SCENTS.map(s => <option key={s.id} value={s.id}>{s.name} ({s.category})</option>)}
|
||||
</TextField>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<FormLabel sx={{ color: 'secondary.main', fontWeight: 600, mb: 1, fontSize: '0.85rem' }}>Secondary Fragrance</FormLabel>
|
||||
<TextField
|
||||
select
|
||||
value={scentSecondary.id}
|
||||
onChange={(e) => setScentSecondary(SCENTS.find(s => s.id === e.target.value) || SCENTS[1])}
|
||||
slotProps={{ select: { native: true } }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ '& select': { py: 1.2 } }}
|
||||
>
|
||||
{SCENTS.filter(s => s.id !== scentPrimary.id).map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name} ({s.category})</option>
|
||||
))}
|
||||
</TextField>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Scent Blend Ratio Slider */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{scentPrimary.name}: {primaryPct}%
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{scentSecondary.name}: {secondaryPct}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={ratio}
|
||||
onChange={(_, val) => setRatio(val as number)}
|
||||
min={40}
|
||||
max={90}
|
||||
color="primary"
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
* Recommend keeping ratios between 50/50 to 80/20 for stable olfactory performance.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Scent Intensity */}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend" sx={{ color: 'secondary.main', fontWeight: 600, mb: 2, fontSize: '0.85rem' }}>
|
||||
Olfactory Concentration (Strength)
|
||||
</FormLabel>
|
||||
<RadioGroup row value={scentStrength} onChange={(e) => setScentStrength(e.target.value)}>
|
||||
<FormControlLabel value="Subtle" control={<Radio size="small" />} label="Subtle (-₹50)" />
|
||||
<FormControlLabel value="Balanced" control={<Radio size="small" />} label="Balanced (+₹0)" />
|
||||
<FormControlLabel value="Intense" control={<Radio size="small" />} label="Intense (+₹100)" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Tab 3: Custom label details */}
|
||||
{activeTab === 2 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3.5 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Custom Label Header"
|
||||
value={labelText}
|
||||
onChange={(e) => setLabelText(e.target.value.substring(0, 24))}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
placeholder="E.g., Amber Evening, Soul Space"
|
||||
helperText="Max 24 characters"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Label Sub-header / Description"
|
||||
value={labelSubText}
|
||||
onChange={(e) => setLabelSubText(e.target.value.substring(0, 32))}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
placeholder="E.g., Hand-Poured for Priya"
|
||||
helperText="Max 32 characters"
|
||||
/>
|
||||
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend" sx={{ color: 'secondary.main', fontWeight: 600, mb: 1.5, fontSize: '0.85rem' }}>
|
||||
Select Label Font Style
|
||||
</FormLabel>
|
||||
<RadioGroup row value={labelFont} onChange={(e) => setLabelFont(e.target.value)}>
|
||||
<FormControlLabel value="serif" control={<Radio size="small" />} label={<span style={{ fontFamily: 'Cormorant Garamond', fontWeight: 'bold' }}>Classical Serif</span>} />
|
||||
<FormControlLabel value="sans" control={<Radio size="small" />} label={<span style={{ fontFamily: 'Inter' }}>Modern Sans-Serif</span>} />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#EFE9F3' }} />
|
||||
|
||||
{/* Quantity and Cart Checkout Controls */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
Total Estimated Cost
|
||||
</Typography>
|
||||
<Typography variant="h4" color="primary" sx={{ fontWeight: 700 }}>
|
||||
{formatCurrency(totalPrice)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
{/* Quantity adjustment */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', border: '1px solid #EFE9F3', borderRadius: '50px', p: 0.5 }}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setQuantity(q => Math.max(1, q - 1))}
|
||||
sx={{ minWidth: 32, p: 0.5, borderRadius: '50%' }}
|
||||
disabled={isB2BMode && quantity <= 20}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<Typography variant="body1" sx={{ px: 2, fontWeight: 600 }}>
|
||||
{quantity}
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setQuantity(q => q + 1)}
|
||||
sx={{ minWidth: 32, p: 0.5, borderRadius: '50%' }}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<MdOutlineShoppingBag />}
|
||||
onClick={handleAddToCart}
|
||||
disabled={isB2BMode && quantity < 20}
|
||||
sx={{ px: 4, py: 1.5 }}
|
||||
>
|
||||
Add Custom Candle
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isB2BMode && quantity < 20 && (
|
||||
<Typography variant="caption" color="error.main" sx={{ mt: 1.5, display: 'block', fontWeight: 500 }}>
|
||||
* Wholesaler mode requires a minimum order of 20 custom candles.
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCandlePage;
|
||||
|
|
@ -0,0 +1,490 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { Container, Grid, Box, Typography, Button, Paper, Card, CardContent, } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { IoIosArrowForward } from 'react-icons/io';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import { mockProducts } from '../../../utils/data';
|
||||
import { formatCurrency } from '../../../utils/helpers';
|
||||
import ProductCard from '../../shop/components/ProductCard';
|
||||
|
||||
const SCENT_PROFILES = [
|
||||
{ id: 'woody', label: 'Woody & Earthy', desc: 'Notes of deodar cedar, assam oud, and sandalwood. Evokes ancient forest walks.', product: 'sandalwood-saffron' },
|
||||
{ id: 'floral', label: 'Floral & Sweet', desc: 'Intoxicating mogra flowers, night-blooming jasmine, and rose petals.', product: 'jasmine-nectar' },
|
||||
{ id: 'oriental', label: 'Spicy & Oriental', desc: 'Malabar cardamom, kashmiri saffron, ginger root, and clove spices.', product: 'cardamom-oud' },
|
||||
{ id: 'citrus', label: 'Citrus & Fresh', desc: 'Zesty Kerala kaffir lime, lemongrass, and clean mountain eucalyptus.', product: 'kerala-coconut-lime' }
|
||||
];
|
||||
|
||||
interface HomePageProps {
|
||||
isB2BMode: boolean;
|
||||
}
|
||||
|
||||
export const HomePage: React.FC<HomePageProps> = ({ isB2BMode }) => {
|
||||
|
||||
const [activeScentId, setActiveScentId] = useState('woody');
|
||||
|
||||
// Filter scent based on interactive select
|
||||
const activeScent = useMemo(() => {
|
||||
return SCENT_PROFILES.find(s => s.id === activeScentId) || SCENT_PROFILES[0];
|
||||
}, [activeScentId]);
|
||||
|
||||
const recommendedScentProduct = useMemo(() => {
|
||||
return mockProducts.find(p => p.id === activeScent.product) || mockProducts[0];
|
||||
}, [activeScent]);
|
||||
|
||||
// Get Best Sellers list
|
||||
const bestSellers = useMemo(() => {
|
||||
return mockProducts.filter(p => p.isBestSeller).slice(0, 3);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Mani Candles | Luxury Handmade Soy Candles India"
|
||||
description="Discover Mani Candles. Hand-poured soy and coconut candles crafted with Kashmiri saffron, Mysore sandalwood, and Madurai jasmine. Custom builder & bulk B2B gifting."
|
||||
/>
|
||||
|
||||
{/* Animated Hero Section */}
|
||||
<Box
|
||||
className="arrival-hero"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: { xs: 'auto', md: '85vh' },
|
||||
minHeight: { xs: 600, md: 700 },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{/* Fog Layers */}
|
||||
<Box className="fog fog-1" />
|
||||
<Box className="fog fog-2" />
|
||||
<Box className="fog fog-3" />
|
||||
|
||||
{/* Embers */}
|
||||
<Box className="embers">
|
||||
{Array.from({ length: 15 }).map((_, i) => (
|
||||
<Box
|
||||
component="span"
|
||||
key={i}
|
||||
sx={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 8}s`,
|
||||
animationDuration: `${8 + Math.random() * 6}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="xl" className="arrival-content" sx={{ display: 'flex', alignItems: 'center', py: { xs: 8, md: 0 }, zIndex: 3 }}>
|
||||
<Grid container spacing={{ xs: 6, md: 10 }} sx={{ alignItems: 'center' }}>
|
||||
{/* Left Column: Brand Copy */}
|
||||
<Grid size={{ xs: 12, md: 6 }} className="arrival-copy" sx={{ textAlign: { xs: 'center', md: 'left' } }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/logo.jpg"
|
||||
alt="Mani Candles Logo"
|
||||
className="arrival-logo"
|
||||
sx={{
|
||||
width: { xs: 100, md: 130 },
|
||||
height: { xs: 100, md: 130 },
|
||||
borderRadius: '50%',
|
||||
border: '2px solid rgba(212, 162, 76, 0.4)',
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
||||
mb: 3,
|
||||
mx: { xs: 'auto', md: 0 },
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontFamily: 'Cormorant Garamond',
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: '2.8rem', sm: '3.8rem', md: '4.8rem' },
|
||||
color: '#FFFFFF',
|
||||
lineHeight: 1.0,
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
Mani Candles Co.
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h2"
|
||||
className="font-script"
|
||||
sx={{
|
||||
fontSize: { xs: '1.8rem', md: '2.5rem' },
|
||||
color: 'secondary.main',
|
||||
mb: 3.5,
|
||||
}}
|
||||
>
|
||||
Handmade • Natural • Heartmade
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.85)',
|
||||
fontSize: '1rem',
|
||||
maxWidth: 480,
|
||||
mx: { xs: 'auto', md: 0 },
|
||||
lineHeight: 1.8,
|
||||
mb: 4.5,
|
||||
}}
|
||||
>
|
||||
Discover slow-poured luxury soy candles crafted with essential botanical extracts and lead-free cotton wicks, designed to illuminate your sanctuary with meditative calm.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2.5, justifyContent: { xs: 'center', md: 'flex-start' }, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/shop"
|
||||
variant="outlined"
|
||||
className="arrival-btn"
|
||||
sx={{
|
||||
color: '#FFFFFF',
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'secondary.main',
|
||||
boxShadow: '0 0 30px rgba(212, 162, 76, 0.4)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Shop Scent Atelier
|
||||
</Button>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/custom-candle"
|
||||
variant="outlined"
|
||||
className="arrival-btn"
|
||||
sx={{
|
||||
color: 'secondary.main',
|
||||
borderColor: 'rgba(212, 162, 76, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'secondary.main',
|
||||
boxShadow: '0 0 30px rgba(212, 162, 76, 0.4)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Customizer
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Right Column: Dynamic Breathing Vessel */}
|
||||
<Grid size={{ xs: 12, md: 6 }} className="arrival-candle-wrap" sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{/* Breathing Aura */}
|
||||
<Box className="arrival-glow" />
|
||||
|
||||
{/* Product Photo */}
|
||||
<Box
|
||||
component="img"
|
||||
src="https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=800"
|
||||
alt="Premium Signature Candle"
|
||||
className="arrival-candle"
|
||||
sx={{
|
||||
width: { xs: 260, sm: 340, md: 400 },
|
||||
height: 'auto',
|
||||
zIndex: 4,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
{/* Bouncing Scroll Indicator */}
|
||||
<Box className="scroll-indicator">
|
||||
Scroll Down
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Featured Collections Section */}
|
||||
<Container maxWidth="lg" sx={{ py: 10 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 8 }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Curated Formulations
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, color: 'secondary.main' }}>
|
||||
Featured Collections
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Card
|
||||
component={RouterLink}
|
||||
to="/shop?category=classic"
|
||||
sx={{ border: '1px solid #E6DFD5', boxShadow: 'none', position: 'relative', borderRadius: 3 }}
|
||||
>
|
||||
<Box sx={{ pt: '120%', backgroundImage: 'url("https://images.unsplash.com/photo-1590794056226-79ef3a8147e1?auto=format&fit=crop&q=80&w=400")', backgroundSize: 'cover', backgroundPosition: 'center' }} />
|
||||
<CardContent sx={{ textAlign: 'center', p: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Classic Collection
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Card
|
||||
component={RouterLink}
|
||||
to="/shop?category=luxury"
|
||||
sx={{ border: '1px solid #E6DFD5', boxShadow: 'none', position: 'relative', borderRadius: 3 }}
|
||||
>
|
||||
<Box sx={{ pt: '120%', backgroundImage: 'url("https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=400")', backgroundSize: 'cover', backgroundPosition: 'center' }} />
|
||||
<CardContent sx={{ textAlign: 'center', p: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Luxury Collection
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Card
|
||||
component={RouterLink}
|
||||
to="/shop?category=seasonal"
|
||||
sx={{ border: '1px solid #E6DFD5', boxShadow: 'none', position: 'relative', borderRadius: 3 }}
|
||||
>
|
||||
<Box sx={{ pt: '120%', backgroundImage: 'url("https://images.unsplash.com/photo-1605651202774-7d573fd3f12d?auto=format&fit=crop&q=80&w=400")', backgroundSize: 'cover', backgroundPosition: 'center' }} />
|
||||
<CardContent sx={{ textAlign: 'center', p: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Seasonal Line
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Card
|
||||
component={RouterLink}
|
||||
to="/shop?category=giftbox"
|
||||
sx={{ border: '1px solid #E6DFD5', boxShadow: 'none', position: 'relative', borderRadius: 3 }}
|
||||
>
|
||||
<Box sx={{ pt: '120%', backgroundImage: 'url("https://images.unsplash.com/photo-1513201099705-a9746e1e201f?auto=format&fit=crop&q=80&w=400")', backgroundSize: 'cover', backgroundPosition: 'center' }} />
|
||||
<CardContent sx={{ textAlign: 'center', p: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
Heritage Gift Boxes
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
{/* Best Sellers */}
|
||||
<Box sx={{ py: 10, backgroundColor: '#FFFFFF', borderTop: '1px solid #E6DFD5', borderBottom: '1px solid #E6DFD5' }}>
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', mb: 6 }}>
|
||||
<Box>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
Atelier Favorites
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 0.5, color: 'secondary.main' }}>
|
||||
Best Selling Pourings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/shop"
|
||||
endIcon={<IoIosArrowForward />}
|
||||
sx={{ color: 'primary.main', fontWeight: 600, textTransform: 'none' }}
|
||||
>
|
||||
View All Atelier
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
{bestSellers.map(product => (
|
||||
<Grid size={{ xs: 12, sm: 4 }} key={product.id}>
|
||||
<ProductCard product={product} isB2BMode={isB2BMode} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* Interactive Olfactory Scent Wheel */}
|
||||
<Container maxWidth="lg" sx={{ py: 12 }}>
|
||||
<Grid container spacing={6} sx={{ alignItems: 'center' }}>
|
||||
{/* Left: Info */}
|
||||
<Grid size={{ xs: 12, md: 5 }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
Olfactory Selector
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 3, color: 'secondary.main' }}>
|
||||
Find Your Olfactory Signature
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="p" sx={{ lineHeight: 1.7, mb: 4 }}>
|
||||
Fragrances govern emotional energy. Click through our primary scent categories below to preview recommended luxury candles based on their olfactory ingredients.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{SCENT_PROFILES.map(scent => (
|
||||
<Paper
|
||||
key={scent.id}
|
||||
variant="outlined"
|
||||
onClick={() => setActiveScentId(scent.id)}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 3,
|
||||
borderColor: activeScentId === scent.id ? 'primary.main' : '#E6DFD5',
|
||||
backgroundColor: activeScentId === scent.id ? 'rgba(142, 117, 87, 0.03)' : '#FFFFFF',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': { borderColor: 'primary.light' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: activeScentId === scent.id ? 'primary.main' : 'secondary.main' }}>
|
||||
{scent.label}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{scent.desc}
|
||||
</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Right: Recommendation Card Display */}
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: { xs: 4, md: 6 },
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #E6DFD5',
|
||||
boxShadow: '0 8px 30px rgba(0,0,0,0.02)',
|
||||
textAlign: 'center',
|
||||
animation: 'fadeIn 0.5s ease',
|
||||
}}
|
||||
>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600, display: 'block', mb: 2 }}>
|
||||
RECOMMENDED FOR YOU
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: 220,
|
||||
height: 220,
|
||||
mx: 'auto',
|
||||
borderRadius: '50%',
|
||||
backgroundImage: `url(${recommendedScentProduct.images[0]})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
border: '4px solid #FAF6F0',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.05)',
|
||||
mb: 4,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mb: 1, color: 'secondary.main' }}>
|
||||
{recommendedScentProduct.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 450, mx: 'auto', mb: 3 }}>
|
||||
{recommendedScentProduct.tagline}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
component={RouterLink}
|
||||
to={`/shop/${recommendedScentProduct.id}`}
|
||||
sx={{ px: 4, py: 1.3 }}
|
||||
>
|
||||
View Scent Details ({formatCurrency(isB2BMode ? recommendedScentProduct.b2bPrice : recommendedScentProduct.price)})
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
{/* Promotes Custom Candle Builder */}
|
||||
<Box
|
||||
sx={{
|
||||
py: 12,
|
||||
backgroundImage: 'linear-gradient(rgba(28, 25, 23, 0.7), rgba(28, 25, 23, 0.7)), url("https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=1200")',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
color: '#FAF6F0',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="overline" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Infinite Blends
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 3 }}>
|
||||
Design Your Signature Scent
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ mb: 5, maxWidth: 600, mx: 'auto', opacity: 0.9, lineHeight: 1.8 }}>
|
||||
Tired of off-the-shelf options? Select custom wax components, blend multiple fragrance oils with precision percentages, and type custom labels in our workspace.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={RouterLink}
|
||||
to="/custom-candle"
|
||||
sx={{ px: 5, py: 1.8, fontSize: '0.8rem', backgroundColor: 'primary.main', color: '#FFFFFF', '&:hover': { backgroundColor: '#FAF6F0', color: 'secondary.main' } }}
|
||||
>
|
||||
Launch Builder Workspace
|
||||
</Button>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* B2B Gateways */}
|
||||
<Container maxWidth="lg" sx={{ py: 12 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 8 }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.2em', fontWeight: 600 }}>
|
||||
Corporate & Hospitality
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, color: 'secondary.main' }}>
|
||||
Corporate & Custom B2B Services
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Paper variant="outlined" sx={{ p: 4, height: '100%', borderRadius: 3, borderColor: '#E6DFD5', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
Corporate Gifting
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flexGrow: 1, lineHeight: 1.6 }}>
|
||||
Logo embossing, luxury rigid boxes, custom inserts, and GST compliance invoices for VIP client lists.
|
||||
</Typography>
|
||||
<Button variant="outlined" color="primary" component={RouterLink} to="/b2b/corporate-gifting">
|
||||
Corporate Services →
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Paper variant="outlined" sx={{ p: 4, height: '100%', borderRadius: 3, borderColor: '#E6DFD5', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
Hotels & Hospitality
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flexGrow: 1, lineHeight: 1.6 }}>
|
||||
Bespoke turn-down mini candles and lobby ambient scenting configurations for resorts and spas.
|
||||
</Typography>
|
||||
<Button variant="outlined" color="primary" component={RouterLink} to="/b2b/hotels-hospitality">
|
||||
Hospitality Scenting →
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Paper variant="outlined" sx={{ p: 4, height: '100%', borderRadius: 3, borderColor: '#E6DFD5', textAlign: 'center', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
Private Label / OEM
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flexGrow: 1, lineHeight: 1.6 }}>
|
||||
Full contract manufacturing, lab stability testing, custom vessel sourcing and small-batch packaging.
|
||||
</Typography>
|
||||
<Button variant="outlined" color="primary" component={RouterLink} to="/b2b/private-label">
|
||||
OEM Private Label →
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import { Card, CardMedia, CardContent, Typography, Box, Avatar } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import type { BlogArticle } from '../../../types';
|
||||
|
||||
interface ArticleCardProps {
|
||||
article: BlogArticle;
|
||||
}
|
||||
|
||||
export const ArticleCard: React.FC<ArticleCardProps> = ({ article }) => {
|
||||
return (
|
||||
<Card
|
||||
component={RouterLink}
|
||||
to={`/journal/${article.slug}`}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '1px solid #E6DFD5',
|
||||
boxShadow: 'none',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: '0 12px 30px rgba(142, 117, 87, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative', pt: '60%', overflow: 'hidden' }}>
|
||||
<CardMedia
|
||||
image={article.coverImage}
|
||||
title={article.title}
|
||||
component="div"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.8s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<CardContent sx={{ p: 4, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
color="primary"
|
||||
sx={{ letterSpacing: '0.12em', fontWeight: 600, fontSize: '0.65rem' }}
|
||||
>
|
||||
{article.category}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{article.readTime}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontFamily: 'Cormorant Garamond',
|
||||
fontWeight: 700,
|
||||
color: 'secondary.main',
|
||||
lineHeight: 1.2,
|
||||
mb: 1.5,
|
||||
fontSize: '1.25rem',
|
||||
}}
|
||||
>
|
||||
{article.title}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
lineHeight: 1.6,
|
||||
mb: 3,
|
||||
flexGrow: 1,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 3,
|
||||
}}
|
||||
>
|
||||
{article.excerpt}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mt: 'auto' }}>
|
||||
<Avatar src={article.author.avatar} sx={{ width: 32, height: 32, border: '1px solid #E6DFD5' }} />
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', color: 'secondary.main' }}>
|
||||
{article.author.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem' }}>
|
||||
{article.author.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleCard;
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Container, Box, Chip, Typography, Avatar, Button, Divider, Grid } from '@mui/material';
|
||||
import { IoIosArrowBack } from 'react-icons/io';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import { mockArticles } from '../../../utils/data';
|
||||
import ArticleCard from '../components/ArticleCard';
|
||||
|
||||
export const ArticleDetailPage: React.FC = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Find active article
|
||||
const article = useMemo(() => {
|
||||
return mockArticles.find((a) => a.slug === slug) || mockArticles[0];
|
||||
}, [slug]);
|
||||
|
||||
// Generate other articles recommendation
|
||||
const recommendations = useMemo(() => {
|
||||
return mockArticles.filter((a) => a.id !== article.id).slice(0, 2);
|
||||
}, [article]);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title={`${article.title} | Mani Candles Journal`}
|
||||
description={article.excerpt}
|
||||
ogImage={article.coverImage}
|
||||
/>
|
||||
|
||||
<Container maxWidth="md">
|
||||
{/* Back Button */}
|
||||
<Button
|
||||
startIcon={<IoIosArrowBack />}
|
||||
onClick={() => navigate('/journal')}
|
||||
sx={{ mb: 4, color: 'text.secondary', textTransform: 'none', '&:hover': { color: 'primary.main' } }}
|
||||
>
|
||||
Back to Journal
|
||||
</Button>
|
||||
|
||||
{/* Article Meta Header */}
|
||||
<Box sx={{ mb: 5 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', mb: 2 }}>
|
||||
<Chip label={article.category} size="small" color="primary" sx={{ borderRadius: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{article.date} • {article.readTime}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
fontFamily: 'Cormorant Garamond',
|
||||
fontWeight: 700,
|
||||
color: 'secondary.main',
|
||||
lineHeight: 1.15,
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{article.title}
|
||||
</Typography>
|
||||
|
||||
{/* Author Block */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar src={article.author.avatar} sx={{ width: 44, height: 44, border: '1px solid #E6DFD5' }} />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main' }}>
|
||||
{article.author.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{article.author.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Cover Photo */}
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: { xs: 240, sm: 400 },
|
||||
borderRadius: 3,
|
||||
border: '1px solid #E6DFD5',
|
||||
backgroundImage: `url(${article.coverImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
mb: 6,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Article Content Body */}
|
||||
<Box
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
lineHeight: 1.8,
|
||||
fontSize: '1.05rem',
|
||||
'& p': { mb: 3 },
|
||||
'& h3': { fontFamily: 'Cormorant Garamond', fontWeight: 700, fontSize: '1.75rem', mt: 4, mb: 2, color: 'secondary.main' },
|
||||
'& ul': { pl: 3, mb: 3 },
|
||||
'& li': { mb: 1 },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: article.content }}
|
||||
/>
|
||||
|
||||
{/* Tag chips */}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mt: 6, mb: 4 }}>
|
||||
{article.tags.map((tag) => (
|
||||
<Chip key={tag} label={`#${tag}`} size="small" variant="outlined" sx={{ borderRadius: 1.5, color: 'text.secondary' }} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 6, borderColor: '#E6DFD5' }} />
|
||||
|
||||
{/* Recommended Reads */}
|
||||
<Box sx={{ mt: 6 }}>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', mb: 4 }}>
|
||||
Related Readings
|
||||
</Typography>
|
||||
<Grid container spacing={4}>
|
||||
{recommendations.map((a) => (
|
||||
<Grid size={{ xs: 12, sm: 6 }} key={a.id}>
|
||||
<ArticleCard article={a} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleDetailPage;
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { Container, Grid, Box, Typography, Paper, Chip, Button } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import ArticleCard from '../components/ArticleCard';
|
||||
import { mockArticles } from '../../../utils/data';
|
||||
|
||||
const CATEGORIES = ['All', 'Lifestyle', 'Craftsmanship', 'Heritage'];
|
||||
|
||||
export const JournalPage: React.FC = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
|
||||
// Find featured post
|
||||
const featuredArticle = useMemo(() => {
|
||||
return mockArticles.find(a => a.featured) || mockArticles[0];
|
||||
}, []);
|
||||
|
||||
// Filter remaining posts
|
||||
const filteredArticles = useMemo(() => {
|
||||
return mockArticles.filter(article => {
|
||||
// Exclude featured article from the grid
|
||||
if (article.id === featuredArticle.id) return false;
|
||||
if (selectedCategory !== 'All' && article.category !== selectedCategory) return false;
|
||||
return true;
|
||||
});
|
||||
}, [selectedCategory, featuredArticle]);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="The Journal | Mani Candles"
|
||||
description="Explore the art of slow living, home aromatherapies, soy wax clean burning science, and Indian olfactory history."
|
||||
/>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{/* Page Title */}
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.25em', fontWeight: 600 }}>
|
||||
Editorial & Circle
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, mb: 2, color: 'secondary.main', fontFamily: 'Cormorant Garamond', fontWeight: 700 }}>
|
||||
The Candle Circle Journal
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 600, mx: 'auto', lineHeight: 1.7 }}>
|
||||
Reflections on botanical alchemy, conscious decor, and the historical archives of scent craftsmanship.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Featured Banner Post */}
|
||||
{featuredArticle && (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: 4,
|
||||
border: '1px solid #E6DFD5',
|
||||
overflow: 'hidden',
|
||||
mb: 8,
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.01)',
|
||||
backgroundColor: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid size={{ xs: 12, md: 7 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: { xs: 260, md: 420 },
|
||||
backgroundImage: `url(${featuredArticle.coverImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 5 }} sx={{ p: { xs: 4, md: 6 }, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
FEATURED READ • {featuredArticle.category}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{featuredArticle.readTime}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component={RouterLink}
|
||||
to={`/journal/${featuredArticle.slug}`}
|
||||
sx={{
|
||||
fontFamily: 'Cormorant Garamond',
|
||||
fontWeight: 700,
|
||||
color: 'secondary.main',
|
||||
textDecoration: 'none',
|
||||
lineHeight: 1.2,
|
||||
mb: 2,
|
||||
'&:hover': { color: 'primary.main' },
|
||||
}}
|
||||
>
|
||||
{featuredArticle.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 4, lineHeight: 1.7 }}>
|
||||
{featuredArticle.excerpt}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
component={RouterLink}
|
||||
to={`/journal/${featuredArticle.slug}`}
|
||||
sx={{ alignSelf: 'flex-start', px: 4 }}
|
||||
>
|
||||
Read Article
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Category Navigation Pills */}
|
||||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', justifyContent: 'center', mb: 5 }}>
|
||||
{CATEGORIES.map(cat => (
|
||||
<Chip
|
||||
key={cat}
|
||||
label={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
color={selectedCategory === cat ? 'primary' : 'default'}
|
||||
variant={selectedCategory === cat ? 'filled' : 'outlined'}
|
||||
sx={{ px: 2, py: 2, fontSize: '0.8rem', cursor: 'pointer' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Grid List */}
|
||||
{filteredArticles.length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No other articles found in this category.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={4}>
|
||||
{filteredArticles.map(article => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 6 }} key={article.id}>
|
||||
<ArticleCard article={article} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default JournalPage;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { Container, Typography, Box, Paper, Divider } from '@mui/material';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
|
||||
export const PrivacyPolicyPage: React.FC = () => {
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||
<SEOHelmet
|
||||
title="Privacy Policy | Mani Candles"
|
||||
description="Learn how Mani Candles collects, utilizes, and protects B2C shopper information and B2B corporate account details."
|
||||
/>
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
Legal & Compliance
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, color: 'secondary.main' }}>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||
Last Updated: July 5, 2026
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: { xs: 3, md: 5 }, borderRadius: 3, backgroundColor: '#FFFFFF', border: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
1. Introduction
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
At Mani Candles, we respect your privacy and are committed to protecting it. This Privacy Policy explains how we collect, use, and safeguard your personal information when you browse our retail B2C catalog, design custom formulations in our interactive candle workspace, or place corporate bulk orders.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
2. Information We Collect
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p">
|
||||
We collect information directly from you when you complete purchases, submit B2B corporate inquiries, register for newsletter benefits, or build custom formulations:
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="div" sx={{ pl: 2, mb: 4 }}>
|
||||
<ul>
|
||||
<li><strong>Contact details:</strong> Name, business email, delivery address, phone number.</li>
|
||||
<li><strong>B2B Account details:</strong> Company name, GST numbers, retail certificates, custom packaging assets.</li>
|
||||
<li><strong>Payment details:</strong> Cryptographically tokenized payment confirmations (credit cards, UPI, net banking). We never store raw payment details.</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
3. How We Use Your Data
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
We use your data to fulfill e-commerce shipments, calculate precise wholesale B2B pricing tiers, customize candle labels, dispatch newsletter invitations, and comply with tax regulations in India. We do not sell or lease your personal information to third-party advertising companies.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
4. Contact Information
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
For queries regarding your personal data deletion or questions about our security standards, contact our privacy desk at:<br />
|
||||
<strong>Email:</strong> privacy@manicandles.com<br />
|
||||
<strong>Address:</strong> Mani Candles Studio, Aluva, Kochi, Kerala, India - 683101
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicyPage;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { Container, Typography, Box, Paper, Divider } from '@mui/material';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
|
||||
export const RefundPolicyPage: React.FC = () => {
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||
<SEOHelmet
|
||||
title="Refund Policy | Mani Candles"
|
||||
description="Learn about our return, refund, and replacement policies for B2C retail candles, personalized formulas, and B2B corporate contracts."
|
||||
/>
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
Legal & Compliance
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, color: 'secondary.main' }}>
|
||||
Refund Policy
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||
Last Updated: July 5, 2026
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: { xs: 3, md: 5 }, borderRadius: 3, backgroundColor: '#FFFFFF', border: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
1. 7-Day Return Policy (B2C)
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p">
|
||||
We want you to love your home scent. If you are not satisfied with your retail B2C order:
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="div" sx={{ pl: 2, mb: 4 }}>
|
||||
<ul>
|
||||
<li>You can return any unburned, unused candle in its original packaging within 7 calendar days of delivery.</li>
|
||||
<li>Due to health and safety regulations, we cannot accept returns for candles that have been lit, burned, or removed from their luxury product sleeve.</li>
|
||||
<li>Return shipping costs are the responsibility of the customer.</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
2. Custom Candle Exclusions
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
Please note that personalized candles designed in our interactive custom builder—where custom scents are blended, or labels are printed with bespoke texts—are classified as custom-built commissions. They are entirely non-returnable and non-refundable unless they suffer from manufacturing defects or transit breakage.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
3. B2B Wholesale Order Policies
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
B2B custom packaging, private label productions, and custom corporate candle batches are non-refundable once the manufacturing contract has been signed and deposits received. We provide pilot product samples before bulk manufacturing to align on scent tolerances.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
4. How to Process a Refund
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
To initiate a return, write to our customer support desk at <strong>support@manicandles.com</strong>. Include your Order ID and photos of the unburned products. Once approved, refunds are credited back to your original payment account within 5-7 bank business days.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default RefundPolicyPage;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { Container, Typography, Box, Paper, Divider } from '@mui/material';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
|
||||
export const ShippingPolicyPage: React.FC = () => {
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||
<SEOHelmet
|
||||
title="Shipping Policy | Mani Candles"
|
||||
description="Review shipping details, shipping fees, estimated delivery schedules across India, and bulk transport parameters for B2B orders."
|
||||
/>
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
Legal & Compliance
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, color: 'secondary.main' }}>
|
||||
Shipping Policy
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||
Last Updated: July 5, 2026
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: { xs: 3, md: 5 }, borderRadius: 3, backgroundColor: '#FFFFFF', border: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
1. B2C Delivery Timelines & Fees
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p">
|
||||
We partner with premium courier services (Blue Dart, Delhivery) to ship across India:
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="div" sx={{ pl: 2, mb: 4 }}>
|
||||
<ul>
|
||||
<li><strong>Shipping Rates:</strong> Complimentary for orders above ₹999. A flat shipping fee of ₹99 is applicable below this amount.</li>
|
||||
<li><strong>Processing Time:</strong> 1-2 business days for ready candles. Customized candles require 3-4 business days for custom printing and drying before shipment.</li>
|
||||
<li><strong>Transit Timelines:</strong> Metro cities (3-5 business days), rest of India (5-7 business days).</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
2. B2B & Bulk Logistics
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
For corporate gifts, hospitality clients, and private labels, shipping is quoted ex-factory Kochi or on actuals. Since large orders are heavy and require wooden pallet packaging, they are dispatched via trusted road logistics carriers (TCI, V-Trans). Transit times range between 5 to 10 days depending on the state destination.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
3. Damage in Transit
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
Candles are fragile glass items. While we pack them with eco-friendly protective layers, if you receive a damaged candle, please email a photo or unboxing video to <strong>orders@manicandles.com</strong> within 48 hours of delivery. We will dispatch a complimentary replacement candle immediately.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShippingPolicyPage;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { Container, Typography, Box, Paper, Divider } from '@mui/material';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
|
||||
export const TermsConditionsPage: React.FC = () => {
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||
<SEOHelmet
|
||||
title="Terms & Conditions | Mani Candles"
|
||||
description="Read the terms of service governing retail shopping, B2B wholesale transactions, custom candle builder policies, and dispute resolution."
|
||||
/>
|
||||
<Box sx={{ mb: 6, textAlign: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
Legal & Compliance
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ mt: 1, color: 'secondary.main' }}>
|
||||
Terms & Conditions
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||
Last Updated: July 5, 2026
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: { xs: 3, md: 5 }, borderRadius: 3, backgroundColor: '#FFFFFF', border: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
1. Terms of Use
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
By accessing and browsing this website, or utilizing our interactive custom candle customized workspace, you agree to comply with our Terms & Conditions. These terms apply to all visitors, B2C retail shoppers, and B2B corporate buyers.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
2. Custom Candle Builder & Intellectual Property
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p">
|
||||
When you use our Custom Candle Builder to input personalized text, upload branding files, or specify fragrance names:
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" component="div" sx={{ pl: 2, mb: 4 }}>
|
||||
<ul>
|
||||
<li>You warrant that you own or have the proper license for any brand logos or text uploaded.</li>
|
||||
<li>We reserve the right to refuse printing any labels containing hate speech, copyrighted brands, or offensive phrasing.</li>
|
||||
<li>Customized orders cannot be cancelled or refunded once wax pouring and printing have begun.</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
3. B2B Wholesale Accounts
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" component="p" sx={{ mb: 4 }}>
|
||||
Our B2B prices are reserved strictly for registered businesses with a valid GSTIN number in India. All corporate quotes are valid for 30 calendar days. Bulk shipments are dispatched only upon receipt of 100% advance payments unless credit terms have been formally approved in writing.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 4, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" sx={{ color: 'secondary.main', mb: 2, fontWeight: 500 }}>
|
||||
4. Jurisdiction
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
These terms shall be governed by and construed in accordance with the laws of India. Any disputes arising out of your purchases or use of this website shall be subject to the exclusive jurisdiction of the courts located in Ernakulam, Kerala, India.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsConditionsPage;
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Card, CardMedia, CardContent, Typography, Box, IconButton, Button, Chip } from '@mui/material';
|
||||
import { IoIosHeartEmpty, IoIosHeart, IoIosEye } from 'react-icons/io';
|
||||
import { MdOutlineShoppingBag } from 'react-icons/md';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import type { Product } from '../../../types';
|
||||
import { useWishlist } from '../../../contexts/WishlistContext';
|
||||
import { useCart } from '../../../contexts/CartContext';
|
||||
import { formatCurrency } from '../../../utils/helpers';
|
||||
import QuickViewModal from './QuickViewModal';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
isB2BMode: boolean;
|
||||
}
|
||||
|
||||
export const ProductCard: React.FC<ProductCardProps> = ({ product, isB2BMode }) => {
|
||||
const { toggleWishlist, isInWishlist } = useWishlist();
|
||||
const { addToCart } = useCart();
|
||||
const [quickViewOpen, setQuickViewOpen] = useState(false);
|
||||
|
||||
const favorited = isInWishlist(product.id);
|
||||
const activePrice = isB2BMode ? product.b2bPrice : product.price;
|
||||
|
||||
const handleAddToCartClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// In B2B mode, add the minimum B2B quantity immediately. In B2C, add 1.
|
||||
const qty = isB2BMode ? product.minB2BQuantity : 1;
|
||||
addToCart(product, qty, isB2BMode ? 'B2B' : 'B2C');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
component={RouterLink}
|
||||
to={`/shop/${product.id}`}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
border: '1px solid #E6DFD5',
|
||||
boxShadow: 'none',
|
||||
'&:hover .product-card-actions': {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Badges (New / Best Seller) */}
|
||||
<Box sx={{ position: 'absolute', top: 12, left: 12, zIndex: 3, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{product.isBestSeller && (
|
||||
<Chip
|
||||
label="Best Seller"
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'secondary.main',
|
||||
color: 'background.default',
|
||||
borderRadius: 1,
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{product.isNew && (
|
||||
<Chip
|
||||
label="New Arrival"
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: '#FFFFFF',
|
||||
borderRadius: 1,
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Wishlist Button */}
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleWishlist(product);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
zIndex: 3,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
color: favorited ? 'error.main' : 'secondary.main',
|
||||
boxShadow: '0 4px 10px rgba(0,0,0,0.05)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.main',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{favorited ? <IoIosHeart size={18} /> : <IoIosHeartEmpty size={18} />}
|
||||
</IconButton>
|
||||
|
||||
{/* Image Frame */}
|
||||
<Box sx={{ position: 'relative', overflow: 'hidden', pt: '110%', backgroundColor: 'background.default' }}>
|
||||
<CardMedia
|
||||
image={product.images[0]}
|
||||
title={product.name}
|
||||
component="div"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.06)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Quick Actions (Slide-up on desktop hover) */}
|
||||
<Box
|
||||
className="product-card-actions"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
right: 12,
|
||||
zIndex: 2,
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
opacity: { xs: 1, md: 0 },
|
||||
transform: { xs: 'none', md: 'translateY(10px)' },
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<MdOutlineShoppingBag />}
|
||||
onClick={handleAddToCartClick}
|
||||
sx={{
|
||||
py: 1,
|
||||
fontSize: '0.7rem',
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
color: 'secondary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'secondary.main',
|
||||
color: 'background.default',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isB2BMode ? `Bulk Add (${product.minB2BQuantity})` : 'Add to Bag'}
|
||||
</Button>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setQuickViewOpen(true);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
color: 'secondary.main',
|
||||
borderRadius: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: 'secondary.main',
|
||||
color: 'background.default',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IoIosEye size={18} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<CardContent sx={{ flexGrow: 1, p: 2.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
color="primary"
|
||||
sx={{
|
||||
letterSpacing: '0.12em',
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{product.category} COLLECTION
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontFamily: 'Cormorant Garamond',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
color: 'secondary.main',
|
||||
my: 0.5,
|
||||
fontSize: '1.1rem',
|
||||
}}
|
||||
>
|
||||
{product.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.78rem', mb: 2, flexGrow: 1, lineClamp: 2, display: '-webkit-box', overflow: 'hidden', WebkitBoxOrient: 'vertical', WebkitLineClamp: 2 }}>
|
||||
{product.tagline}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', mt: 'auto' }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main', fontSize: '1rem' }}>
|
||||
{formatCurrency(activePrice)}
|
||||
</Typography>
|
||||
{isB2BMode && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem', display: 'block' }}>
|
||||
Min order: {product.minB2BQuantity} pcs
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
{product.weight}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick View Dialog */}
|
||||
<QuickViewModal
|
||||
open={quickViewOpen}
|
||||
onClose={() => setQuickViewOpen(false)}
|
||||
product={product}
|
||||
isB2BMode={isB2BMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Box,
|
||||
Typography,
|
||||
Grid,
|
||||
Button,
|
||||
IconButton,
|
||||
Divider,
|
||||
Rating,
|
||||
} from '@mui/material';
|
||||
import { MdClose, MdOutlineShoppingBag, MdAdd, MdRemove } from 'react-icons/md';
|
||||
import type { Product } from '../../../types';
|
||||
import { useCart } from '../../../contexts/CartContext';
|
||||
import { formatCurrency } from '../../../utils/helpers';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface QuickViewModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
product: Product;
|
||||
isB2BMode: boolean;
|
||||
}
|
||||
|
||||
export const QuickViewModal: React.FC<QuickViewModalProps> = ({ open, onClose, product, isB2BMode }) => {
|
||||
const navigate = useNavigate();
|
||||
const { addToCart } = useCart();
|
||||
const [quantity, setQuantity] = useState(isB2BMode ? product.minB2BQuantity : 1);
|
||||
|
||||
const activePrice = isB2BMode ? product.b2bPrice : product.price;
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToCart(product, quantity, isB2BMode ? 'B2B' : 'B2C');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDetailsClick = () => {
|
||||
onClose();
|
||||
navigate(`/shop/${product.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
slotProps={{ paper: { sx: {
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'background.default',
|
||||
border: '1px solid #E6DFD5',
|
||||
} } }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', p: 1 }}>
|
||||
<IconButton onClick={onClose}>
|
||||
<MdClose size={22} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<DialogContent sx={{ p: { xs: 3, md: 5 }, pt: 0 }}>
|
||||
<Grid container spacing={4}>
|
||||
{/* Product Image Panel */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
pt: '100%',
|
||||
backgroundImage: `url(${product.images[0]})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
borderRadius: 3,
|
||||
border: '1px solid #E6DFD5',
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Product Info Panel */}
|
||||
<Grid size={{ xs: 12, md: 6 }} sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.12em', fontWeight: 600 }}>
|
||||
{product.category} Collection
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mt: 1, mb: 0.5, color: 'secondary.main' }}>
|
||||
{product.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Rating value={product.rating} readOnly size="small" precision={0.1} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
({product.reviewsCount} verified reviews)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 1.5, borderColor: '#E6DFD5' }} />
|
||||
|
||||
<Typography variant="h5" color="primary" sx={{ fontWeight: 700, mb: 2 }}>
|
||||
{formatCurrency(activePrice)} {isB2BMode && <span style={{ fontSize: '0.75rem', fontWeight: 'normal', color: '#5E5852' }}>/ Unit (Min Qty: {product.minB2BQuantity})</span>}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3, lineHeight: 1.6 }}>
|
||||
{product.description}
|
||||
</Typography>
|
||||
|
||||
{/* Quick Specs */}
|
||||
<Grid container spacing={1.5} sx={{ mb: 4 }}>
|
||||
<Grid size={6}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Wax Composition</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{product.waxType}</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Burn Duration</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>~{product.burnTime} Hours</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Weight</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{product.weight}</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Olfactory Profile</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, textTransform: 'capitalize' }}>{product.fragranceProfile}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Actions Bar */}
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 'auto' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', border: '1px solid #E6DFD5', borderRadius: '50px', p: 0.5 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setQuantity(q => Math.max(isB2BMode ? product.minB2BQuantity : 1, q - 1))}
|
||||
disabled={isB2BMode && quantity <= product.minB2BQuantity}
|
||||
>
|
||||
<MdRemove size={14} />
|
||||
</IconButton>
|
||||
<Typography variant="body2" sx={{ px: 2, fontWeight: 600 }}>
|
||||
{quantity}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={() => setQuantity(q => q + 1)}>
|
||||
<MdAdd size={14} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<MdOutlineShoppingBag />}
|
||||
onClick={handleAddToCart}
|
||||
sx={{ py: 1.5, fontSize: '0.85rem' }}
|
||||
>
|
||||
Add to Bag
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleDetailsClick}
|
||||
sx={{ mt: 2, color: 'text.secondary', fontSize: '0.78rem', textTransform: 'none' }}
|
||||
>
|
||||
View Full Fragrance Notes & Details →
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickViewModal;
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Rating,
|
||||
Divider,
|
||||
Tabs,
|
||||
Tab,
|
||||
TextField,
|
||||
Chip,
|
||||
Alert,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { MdOutlineShoppingBag, MdAdd, MdRemove } from 'react-icons/md';
|
||||
import { IoIosHeartEmpty, IoIosHeart, IoLogoWhatsapp } from 'react-icons/io';
|
||||
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import { mockProducts } from '../../../utils/data';
|
||||
import { useCart } from '../../../contexts/CartContext';
|
||||
import { useWishlist } from '../../../contexts/WishlistContext';
|
||||
import { formatCurrency } from '../../../utils/helpers';
|
||||
import ProductCard from '../components/ProductCard';
|
||||
|
||||
interface ProductDetailPageProps {
|
||||
isB2BMode: boolean;
|
||||
}
|
||||
|
||||
export const ProductDetailPage: React.FC<ProductDetailPageProps> = ({ isB2BMode }) => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { addToCart } = useCart();
|
||||
const { toggleWishlist, isInWishlist } = useWishlist();
|
||||
|
||||
// Find active product
|
||||
const product = useMemo(() => {
|
||||
return mockProducts.find((p) => p.id === id) || mockProducts[0];
|
||||
}, [id]);
|
||||
|
||||
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
||||
const [quantity, setQuantity] = useState(isB2BMode ? product.minB2BQuantity : 1);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [b2bInquiryOpen, setB2BInquiryOpen] = useState(false);
|
||||
const [inquiryFormSent, setInquiryFormSent] = useState(false);
|
||||
|
||||
// Sync B2B minimum quantity
|
||||
React.useEffect(() => {
|
||||
setQuantity(isB2BMode ? product.minB2BQuantity : 1);
|
||||
}, [isB2BMode, product]);
|
||||
|
||||
const isFavorited = isInWishlist(product.id);
|
||||
const activePrice = isB2BMode ? product.b2bPrice : product.price;
|
||||
|
||||
// Pre-fill WhatsApp message
|
||||
const whatsappUrl = useMemo(() => {
|
||||
const phoneNumber = '919999999999';
|
||||
const text = encodeURIComponent(
|
||||
`Hi Mani Candles, I am interested in purchasing ${product.name} (ID: ${product.id}). Mode: ${isB2BMode ? 'B2B Wholesale' : 'B2C Retail'}. Please share availability.`
|
||||
);
|
||||
return `https://wa.me/${phoneNumber}?text=${text}`;
|
||||
}, [product, isB2BMode]);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToCart(product, quantity, isB2BMode ? 'B2B' : 'B2C');
|
||||
};
|
||||
|
||||
const handleB2BInquirySubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setInquiryFormSent(true);
|
||||
};
|
||||
|
||||
const handleCloseB2BInquiry = () => {
|
||||
setB2BInquiryOpen(false);
|
||||
setInquiryFormSent(false);
|
||||
};
|
||||
|
||||
// Generate related items (excluding current product)
|
||||
const relatedProducts = useMemo(() => {
|
||||
return mockProducts.filter((p) => p.id !== product.id).slice(0, 3);
|
||||
}, [product]);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title={`${product.name} | Premium Handmade Candle India`}
|
||||
description={`${product.name} - ${product.tagline} Made with ${product.waxType} and natural fragrances. Buy online or request B2B corporate pricing.`}
|
||||
ogImage={product.images[0]}
|
||||
/>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
{isB2BMode && (
|
||||
<Alert severity="info" sx={{ mb: 4, borderRadius: 2 }}>
|
||||
<strong>Corporate Wholesale view active:</strong> Special discounted price of {formatCurrency(product.b2bPrice)} per unit is unlocked for minimum batch orders of <strong>{product.minB2BQuantity} items</strong>.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={5}>
|
||||
{/* Left Column: Image Gallery */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{/* Main Image Frame */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
pt: '100%',
|
||||
borderRadius: 3,
|
||||
border: '1px solid #E6DFD5',
|
||||
backgroundImage: `url(${product.images[activeImageIndex]})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.02)',
|
||||
transition: 'background-image 0.4s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Thumbnails grid */}
|
||||
<Box sx={{ display: 'flex', gap: 2, overflowX: 'auto', pb: 1 }}>
|
||||
{product.images.map((img, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
onClick={() => setActiveImageIndex(index)}
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 2,
|
||||
border: activeImageIndex === index ? '2px solid #8E7557' : '1px solid #E6DFD5',
|
||||
backgroundImage: `url(${img})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': { borderColor: 'primary.light' },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Right Column: Spec / Details Panel */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="overline" color="primary" sx={{ letterSpacing: '0.15em', fontWeight: 600 }}>
|
||||
{product.category} COLLECTION
|
||||
</Typography>
|
||||
|
||||
<IconButton onClick={() => toggleWishlist(product)} sx={{ border: '1px solid #E6DFD5', color: isFavorited ? 'error.main' : 'secondary.main' }}>
|
||||
{isFavorited ? <IoIosHeart size={20} /> : <IoIosHeartEmpty size={20} />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h3" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', lineHeight: 1.1 }}>
|
||||
{product.name}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Rating value={product.rating} readOnly precision={0.1} size="small" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{product.rating} ({product.reviewsCount} verified reviews)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h4" color="primary" sx={{ fontWeight: 700 }}>
|
||||
{formatCurrency(activePrice)}{' '}
|
||||
{isB2BMode && (
|
||||
<Typography component="span" variant="caption" color="text.secondary" sx={{ fontSize: '0.75rem', ml: 1 }}>
|
||||
/ unit (Min: {product.minB2BQuantity} units)
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" sx={{ lineHeight: 1.7 }}>
|
||||
{product.description}
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 1.5, borderColor: '#E6DFD5' }} />
|
||||
|
||||
{/* Scent Architecture */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, letterSpacing: '0.05em', textTransform: 'uppercase' }}>
|
||||
Scent Profile & Architecture
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<Typography variant="caption" color="primary" sx={{ fontWeight: 700, display: 'block', mb: 0.5 }}>TOP NOTE</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{product.fragranceNotes.top.join(', ')}</Typography>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography variant="caption" color="primary" sx={{ fontWeight: 700, display: 'block', mb: 0.5 }}>HEART NOTE</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{product.fragranceNotes.middle.join(', ')}</Typography>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography variant="caption" color="primary" sx={{ fontWeight: 700, display: 'block', mb: 0.5 }}>BASE NOTE</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{product.fragranceNotes.base.join(', ')}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 1.5, borderColor: '#E6DFD5' }} />
|
||||
|
||||
{/* E-commerce Purchase Controls */}
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', border: '1px solid #E6DFD5', borderRadius: '50px', p: 0.5 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setQuantity((q) => Math.max(isB2BMode ? product.minB2BQuantity : 1, q - 1))}
|
||||
disabled={isB2BMode && quantity <= product.minB2BQuantity}
|
||||
>
|
||||
<MdRemove size={16} />
|
||||
</IconButton>
|
||||
<Typography variant="body1" sx={{ px: 2.5, fontWeight: 600 }}>
|
||||
{quantity}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={() => setQuantity((q) => q + 1)}>
|
||||
<MdAdd size={16} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<MdOutlineShoppingBag />}
|
||||
onClick={handleAddToCart}
|
||||
sx={{ py: 1.6, fontSize: '0.85rem', letterSpacing: '0.05em' }}
|
||||
>
|
||||
Add to Bag
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* WhatsApp Checkout Integration & B2B Inquiry Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="success"
|
||||
fullWidth
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
startIcon={<IoLogoWhatsapp />}
|
||||
sx={{
|
||||
borderColor: '#25D366',
|
||||
color: '#25D366',
|
||||
py: 1.2,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
borderColor: '#128C7E',
|
||||
backgroundColor: 'rgba(37, 211, 102, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Quick Inquiry on WhatsApp
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => setB2BInquiryOpen(true)}
|
||||
sx={{ py: 1.2, textTransform: 'none' }}
|
||||
>
|
||||
Request Bulk / Sample Pricing
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Specifications Tab & FAQ Accordion Section */}
|
||||
<Box sx={{ mt: 10 }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, val) => setActiveTab(val)}
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
sx={{ borderBottom: '1px solid #E6DFD5', mb: 4 }}
|
||||
>
|
||||
<Tab label="Detailed Specs" />
|
||||
<Tab label="Candle Care Guide" />
|
||||
<Tab label="Ingredients" />
|
||||
<Tab label={`Reviews (${product.reviews.length})`} />
|
||||
</Tabs>
|
||||
|
||||
{activeTab === 0 && (
|
||||
<Grid container spacing={4} sx={{ py: 2 }}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pb: 1, borderBottom: '1px solid rgba(230, 223, 213, 0.5)' }}>
|
||||
<Typography variant="body2" color="text.secondary">Vessel Style</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>Artisanal Clay/Glass Jars</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pb: 1, borderBottom: '1px solid rgba(230, 223, 213, 0.5)' }}>
|
||||
<Typography variant="body2" color="text.secondary">Net Weight</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{product.weight}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pb: 1, borderBottom: '1px solid rgba(230, 223, 213, 0.5)' }}>
|
||||
<Typography variant="body2" color="text.secondary">Wax Base</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{product.waxType}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pb: 1, borderBottom: '1px solid rgba(230, 223, 213, 0.5)' }}>
|
||||
<Typography variant="body2" color="text.secondary">Burn Duration</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>~{product.burnTime} Hours</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pb: 1, borderBottom: '1px solid rgba(230, 223, 213, 0.5)' }}>
|
||||
<Typography variant="body2" color="text.secondary">Fragrance Composition</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>100% Botanical Extracts & Essential Oils</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pb: 1, borderBottom: '1px solid rgba(230, 223, 213, 0.5)' }}>
|
||||
<Typography variant="body2" color="text.secondary">Wick Material</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>Organic Lead-free Cotton</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<Box sx={{ py: 2, maxWidth: 800 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main', mb: 2 }}>
|
||||
HOW TO GET THE PERFECT BURN
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{product.careInstructions.map((inst, index) => (
|
||||
<Typography key={index} variant="body2" color="text.secondary" sx={{ display: 'flex', gap: 1.5, alignItems: 'flex-start' }}>
|
||||
<strong>{index + 1}.</strong> {inst}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTab === 2 && (
|
||||
<Box sx={{ py: 2, maxWidth: 800 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.8 }}>
|
||||
We believe in complete ingredient transparency. Mani Candles are formulated using pure natural waxes and high-grade essential oils. This candle contains:
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', mt: 3 }}>
|
||||
{product.ingredients.map((ing, i) => (
|
||||
<Chip key={i} label={ing} variant="outlined" sx={{ borderColor: '#C5A880', color: 'primary.main', borderRadius: 2 }} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTab === 3 && (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, mb: 3 }}>
|
||||
Client Testimonials & Feedback
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{product.reviews.map((rev) => (
|
||||
<Box key={rev.id} sx={{ pb: 3, borderBottom: '1px solid rgba(230, 223, 213, 0.4)' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{rev.author}{' '}
|
||||
{rev.verified && (
|
||||
<Chip label="Verified Purchase" size="small" color="primary" variant="outlined" sx={{ height: 16, fontSize: '0.55rem', ml: 1, borderRadius: 1 }} />
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{rev.date}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Rating value={rev.rating} readOnly size="small" sx={{ mb: 1.5 }} />
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
|
||||
"{rev.comment}"
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Related Products Grid */}
|
||||
<Box sx={{ mt: 10, pt: 8, borderTop: '1px solid #E6DFD5' }}>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main', mb: 4, textAlign: 'center' }}>
|
||||
Olfactory Pairings & Recommendations
|
||||
</Typography>
|
||||
<Grid container spacing={4}>
|
||||
{relatedProducts.map((p) => (
|
||||
<Grid size={{ xs: 12, sm: 4 }} key={p.id}>
|
||||
<ProductCard product={p} isB2BMode={isB2BMode} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* B2B Request Quote Dialog */}
|
||||
<Dialog open={b2bInquiryOpen} onClose={handleCloseB2BInquiry} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, fontSize: '1.5rem' }}>
|
||||
Bulk / Custom Sample Inquiry
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{inquiryFormSent ? (
|
||||
<Box sx={{ py: 3, textAlign: 'center' }}>
|
||||
<Typography variant="subtitle1" color="primary" sx={{ fontWeight: 600 }}>
|
||||
Inquiry Logged Successfully
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1.5 }}>
|
||||
Our corporate sales desk will dispatch catalog pricing and sample coordinates to your company email shortly.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<form onSubmit={handleB2BInquirySubmit}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5, mt: 1.5 }}>
|
||||
<TextField fullWidth label="Company Name" required size="small" />
|
||||
<TextField fullWidth label="Contact Person" required size="small" />
|
||||
<TextField fullWidth label="Corporate Email" type="email" required size="small" />
|
||||
<TextField fullWidth label="Estimated Quantity" type="number" required size="small" helperText={`Minimum bulk batch is ${product.minB2BQuantity} items`} />
|
||||
<TextField fullWidth label="Special Packaging / Branding Details" multiline rows={3} size="small" />
|
||||
</Box>
|
||||
<DialogActions sx={{ px: 0, pt: 3 }}>
|
||||
<Button onClick={handleCloseB2BInquiry} color="inherit">Cancel</Button>
|
||||
<Button type="submit" variant="contained" color="primary">Submit Request</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetailPage;
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Slider,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Button,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { IoIosFunnel } from 'react-icons/io';
|
||||
import SEOHelmet from '../../../components/common/SEOHelmet';
|
||||
import ProductCard from '../components/ProductCard';
|
||||
import { mockProducts } from '../../../utils/data';
|
||||
|
||||
const CATEGORIES = [
|
||||
{ id: 'classic', label: 'Classic Collection' },
|
||||
{ id: 'luxury', label: 'Luxury Collection' },
|
||||
{ id: 'seasonal', label: 'Seasonal Line' },
|
||||
{ id: 'giftbox', label: 'Heritage Gift Boxes' }
|
||||
];
|
||||
|
||||
const FRAGRANCES = [
|
||||
{ id: 'woody', label: 'Woody' },
|
||||
{ id: 'floral', label: 'Floral' },
|
||||
{ id: 'citrus', label: 'Citrus' },
|
||||
{ id: 'oriental', label: 'Oriental' },
|
||||
{ id: 'fresh', label: 'Fresh & Airy' }
|
||||
];
|
||||
|
||||
const WAXES = [
|
||||
{ id: 'Soy Wax', label: '100% Soy Wax' },
|
||||
{ id: 'Coconut Wax', label: 'Coconut Wax' },
|
||||
{ id: 'Beeswax', label: 'Raw Beeswax' },
|
||||
{ id: 'Blend', label: 'Soy/Coconut Blend' }
|
||||
];
|
||||
|
||||
interface ShopPageProps {
|
||||
isB2BMode: boolean;
|
||||
}
|
||||
|
||||
export const ShopPage: React.FC<ShopPageProps> = ({ isB2BMode }) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// Filters State
|
||||
const [searchQuery, setSearchQuery] = useState(searchParams.get('search') || '');
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [selectedFragrances, setSelectedFragrances] = useState<string[]>([]);
|
||||
const [selectedWaxes, setSelectedWaxes] = useState<string[]>([]);
|
||||
const [burnTimeRange, setBurnTimeRange] = useState<number[]>([20, 120]);
|
||||
const [sortBy, setSortBy] = useState('popular');
|
||||
|
||||
// Sync with URL parameters
|
||||
useEffect(() => {
|
||||
const categoryParam = searchParams.get('category');
|
||||
if (categoryParam) {
|
||||
setSelectedCategories([categoryParam]);
|
||||
} else {
|
||||
setSelectedCategories([]);
|
||||
}
|
||||
|
||||
const searchParam = searchParams.get('search');
|
||||
if (searchParam) {
|
||||
setSearchQuery(searchParam);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// Checkbox handlers
|
||||
const handleCategoryChange = (cat: string) => {
|
||||
setSelectedCategories(prev =>
|
||||
prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat]
|
||||
);
|
||||
};
|
||||
|
||||
const handleFragranceChange = (frag: string) => {
|
||||
setSelectedFragrances(prev =>
|
||||
prev.includes(frag) ? prev.filter(f => f !== frag) : [...prev, frag]
|
||||
);
|
||||
};
|
||||
|
||||
const handleWaxChange = (wax: string) => {
|
||||
setSelectedWaxes(prev =>
|
||||
prev.includes(wax) ? prev.filter(w => w !== wax) : [...prev, wax]
|
||||
);
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setSearchQuery('');
|
||||
setSelectedCategories([]);
|
||||
setSelectedFragrances([]);
|
||||
setSelectedWaxes([]);
|
||||
setBurnTimeRange([20, 120]);
|
||||
setSortBy('popular');
|
||||
setSearchParams({});
|
||||
};
|
||||
|
||||
// Filtered Products computation
|
||||
const filteredProducts = useMemo(() => {
|
||||
return mockProducts
|
||||
.filter(product => {
|
||||
// Search filter
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const matchName = product.name.toLowerCase().includes(query);
|
||||
const matchTagline = product.tagline.toLowerCase().includes(query);
|
||||
const matchProfile = product.fragranceProfile.toLowerCase().includes(query);
|
||||
if (!matchName && !matchTagline && !matchProfile) return false;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (selectedCategories.length > 0 && !selectedCategories.includes(product.category)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fragrance filter
|
||||
if (selectedFragrances.length > 0 && !selectedFragrances.includes(product.fragranceProfile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wax filter
|
||||
if (selectedWaxes.length > 0 && !selectedWaxes.includes(product.waxType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Burn time filter
|
||||
if (product.burnTime < burnTimeRange[0] || product.burnTime > burnTimeRange[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aPrice = isB2BMode ? a.b2bPrice : a.price;
|
||||
const bPrice = isB2BMode ? b.b2bPrice : b.price;
|
||||
|
||||
if (sortBy === 'price-low') return aPrice - bPrice;
|
||||
if (sortBy === 'price-high') return bPrice - aPrice;
|
||||
if (sortBy === 'rating') return b.rating - a.rating;
|
||||
// Default popular/bestseller
|
||||
return (b.isBestSeller ? 1 : 0) - (a.isBestSeller ? 1 : 0);
|
||||
});
|
||||
}, [searchQuery, selectedCategories, selectedFragrances, selectedWaxes, burnTimeRange, sortBy, isB2BMode]);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 6, backgroundColor: 'background.default' }}>
|
||||
<SEOHelmet
|
||||
title="Shop Luxury Handmade Candles | Mani Candles"
|
||||
description="Browse our hand-poured soy candle collections. Filter by woody, floral or citrus fragrance notes, burn times, and retail or B2B pricing."
|
||||
/>
|
||||
<Container maxWidth="xl">
|
||||
<Grid container spacing={4}>
|
||||
{/* Desktop Filter Sidebar */}
|
||||
<Grid size={{ xs: 12, md: 3 }} sx={{ display: { xs: 'none', md: 'block' } }}>
|
||||
<Paper variant="outlined" sx={{ p: 3, borderRadius: 3, borderColor: '#E6DFD5', backgroundColor: '#FFFFFF' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, letterSpacing: '0.05em' }}>
|
||||
FILTERS
|
||||
</Typography>
|
||||
<Button size="small" onClick={clearAllFilters} sx={{ fontSize: '0.75rem', textTransform: 'none', color: 'text.secondary' }}>
|
||||
Clear All
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Text Search inside sidebar */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Fuzzy search catalog..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'background.default',
|
||||
'& fieldset': { borderColor: '#E6DFD5' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Categories Filter */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1.5, letterSpacing: '0.05em' }}>
|
||||
CATEGORY
|
||||
</Typography>
|
||||
<FormGroup>
|
||||
{CATEGORIES.map(cat => (
|
||||
<FormControlLabel
|
||||
key={cat.id}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedCategories.includes(cat.id)}
|
||||
onChange={() => handleCategoryChange(cat.id)}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" color="text.secondary">{cat.label}</Typography>}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3, borderColor: '#E6DFD5' }} />
|
||||
|
||||
{/* Fragrance Profile */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1.5, letterSpacing: '0.05em' }}>
|
||||
OLFACTORY NOTES
|
||||
</Typography>
|
||||
<FormGroup>
|
||||
{FRAGRANCES.map(frag => (
|
||||
<FormControlLabel
|
||||
key={frag.id}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedFragrances.includes(frag.id)}
|
||||
onChange={() => handleFragranceChange(frag.id)}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" color="text.secondary">{frag.label}</Typography>}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3, borderColor: '#E6DFD5' }} />
|
||||
|
||||
{/* Wax Type */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1.5, letterSpacing: '0.05em' }}>
|
||||
WAX INGREDIENT
|
||||
</Typography>
|
||||
<FormGroup>
|
||||
{WAXES.map(wax => (
|
||||
<FormControlLabel
|
||||
key={wax.id}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedWaxes.includes(wax.id)}
|
||||
onChange={() => handleWaxChange(wax.id)}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" color="text.secondary">{wax.label}</Typography>}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3, borderColor: '#E6DFD5' }} />
|
||||
|
||||
{/* Burn Time Slider */}
|
||||
<Box sx={{ px: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 2, letterSpacing: '0.05em' }}>
|
||||
BURN TIME (HOURS)
|
||||
</Typography>
|
||||
<Slider
|
||||
value={burnTimeRange}
|
||||
onChange={(_, val) => setBurnTimeRange(val as number[])}
|
||||
valueLabelDisplay="auto"
|
||||
min={20}
|
||||
max={120}
|
||||
color="primary"
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">{burnTimeRange[0]} hrs</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{burnTimeRange[1]} hrs</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Catalog Right Panel */}
|
||||
<Grid size={{ xs: 12, md: 9 }}>
|
||||
{/* Top Toolbar */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 4,
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{ fontFamily: 'Cormorant Garamond', fontWeight: 700, color: 'secondary.main' }}>
|
||||
The Candle Atelier
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Showing {filteredProducts.length} premium pouring formulas
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
{/* Mobile Filter Button */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {}}
|
||||
startIcon={<IoIosFunnel />}
|
||||
sx={{ display: { xs: 'flex', md: 'none' }, borderRadius: 2, borderColor: '#E6DFD5', color: 'secondary.main' }}
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
|
||||
{/* Sort selector */}
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
variant="outlined"
|
||||
label="Sort By"
|
||||
sx={{
|
||||
width: 180,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2,
|
||||
'& fieldset': { borderColor: '#E6DFD5' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="popular">Best Sellers</MenuItem>
|
||||
<MenuItem value="price-low">Price: Low to High</MenuItem>
|
||||
<MenuItem value="price-high">Price: High to Low</MenuItem>
|
||||
<MenuItem value="rating">Top Rated</MenuItem>
|
||||
</TextField>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Catalog Grid */}
|
||||
{filteredProducts.length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', py: 12, border: '1px dashed #E6DFD5', borderRadius: 4, backgroundColor: '#FFFFFF' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
No candles match your filters
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Try adjusting your burn times, fragrance categories, or clearing queries.
|
||||
</Typography>
|
||||
<Button variant="contained" color="primary" onClick={clearAllFilters}>
|
||||
Clear All Filters
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
{filteredProducts.map(product => (
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 4 }} key={product.id}>
|
||||
<ProductCard product={product} isB2BMode={isB2BMode} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShopPage;
|
||||
|
|
@ -0,0 +1,576 @@
|
|||
:root {
|
||||
--brand-purple: #5B1E8C;
|
||||
--brand-purple-dark: #3A1260;
|
||||
--brand-purple-mid: #7B3DB8;
|
||||
--brand-lavender: #DCC6F2;
|
||||
--brand-light-lavender: #EFE4FA;
|
||||
--brand-card: #F8F2FD;
|
||||
--brand-gold: #D4A24C;
|
||||
--brand-gold-light: #FFD36B;
|
||||
--brand-gold-core: #FFF3C4;
|
||||
--brand-text: #2D0D52;
|
||||
--brand-muted: #7B5A9E;
|
||||
--brand-border: #D5BEF0;
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Cormorant Garamond', serif;
|
||||
--font-display: 'Cinzel', serif;
|
||||
--font-script: 'Great Vibes', cursive;
|
||||
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light;
|
||||
color: var(--brand-text);
|
||||
background-color: var(--brand-light-lavender);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom Fine Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--brand-light-lavender);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--brand-muted);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--brand-purple);
|
||||
}
|
||||
|
||||
/* Glassmorphism Styles */
|
||||
.glass-header {
|
||||
background: rgba(239, 228, 250, 0.85) !important;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.7) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(213, 190, 240, 0.6) !important;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
SCROLL BURNING CANDLE STYLES
|
||||
---------------------------------------------------- */
|
||||
.scroll-candle-wrap {
|
||||
position: fixed;
|
||||
right: 28px; /* Far right edge */
|
||||
bottom: 16px;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 0 12px rgba(91, 30, 140, 0.25));
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
/* Responsive adjustment to hide scroll candle on mobile screens */
|
||||
@media (max-width: 600px) {
|
||||
.scroll-candle-wrap {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.candle-flame-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: -2px;
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.flame-glow {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255, 200, 50, 0.45) 0%, transparent 70%);
|
||||
animation: glow-pulse 1.8s ease-in-out infinite alternate;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0% {
|
||||
transform: translate(-50%) scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%) scale(1.35);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.flame-outer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 18px;
|
||||
height: 34px;
|
||||
background: linear-gradient(to top, #ff6a00, #fa0, #ffe066);
|
||||
border-radius: 50% 50% 30% 30% / 60% 60% 40% 40%;
|
||||
transform-origin: bottom center;
|
||||
animation: flicker-outer 0.9s ease-in-out infinite alternate;
|
||||
box-shadow: 0 0 8px 3px rgba(255, 140, 0, 0.6), 0 0 18px 6px rgba(255, 100, 0, 0.4);
|
||||
}
|
||||
|
||||
@keyframes flicker-outer {
|
||||
0% {
|
||||
transform: scaleX(1) scaleY(1) rotate(-2deg);
|
||||
}
|
||||
25% {
|
||||
transform: scaleX(0.88) scaleY(1.05) rotate(2deg);
|
||||
}
|
||||
50% {
|
||||
transform: scaleX(1.05) scaleY(0.95) rotate(-1deg);
|
||||
}
|
||||
75% {
|
||||
transform: scaleX(0.92) scaleY(1.08) rotate(3deg);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1.02) scaleY(1.02) rotate(-2deg);
|
||||
}
|
||||
}
|
||||
|
||||
.flame-inner {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
z-index: 3;
|
||||
width: 8px;
|
||||
height: 18px;
|
||||
background: linear-gradient(to top, #fff8c5, #fff, rgba(255, 240, 240, 0));
|
||||
border-radius: 50% 50% 30% 30% / 60% 60% 40% 40%;
|
||||
animation: flicker-inner 0.7s ease-in-out infinite alternate;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes flicker-inner {
|
||||
0% {
|
||||
transform: translate(-50%) scaleX(1) scaleY(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%) scaleX(0.85) scaleY(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%) scaleX(1.1) scaleY(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.smoke-wrap {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.smoke-particle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(200, 180, 220, 0.45);
|
||||
transform: translate(-50%);
|
||||
animation: smoke-rise 2.4s ease-out infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.smoke-particle:nth-child(2) {
|
||||
animation-delay: 0.8s;
|
||||
left: 40%;
|
||||
}
|
||||
|
||||
.smoke-particle:nth-child(3) {
|
||||
animation-delay: 1.6s;
|
||||
left: 60%;
|
||||
}
|
||||
|
||||
@keyframes smoke-rise {
|
||||
0% {
|
||||
transform: translate(-50%) translateY(0) scale(0.6);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%) translateY(-22px) scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.candle-wick {
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: linear-gradient(to bottom, #3a2000, #1a0f00);
|
||||
border-radius: 1px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.candle-wick-holder {
|
||||
width: 12px;
|
||||
height: 3px;
|
||||
background: linear-gradient(to right, #bf953f, #fcf6ba, #aa771c);
|
||||
border-radius: 1px;
|
||||
margin-bottom: -1px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.candle-body {
|
||||
position: relative;
|
||||
width: 12px; /* Sleek, tall luxury rod */
|
||||
background: linear-gradient(90deg,
|
||||
#3a1260 0%,
|
||||
#5b1e8c 30%,
|
||||
#7b3db8 50%,
|
||||
#5b1e8c 70%,
|
||||
#1e0a27 100%
|
||||
);
|
||||
border-radius: 2px 2px 3px 3px;
|
||||
transition: height 0.15s ease-out;
|
||||
box-shadow:
|
||||
inset -1px 0 3px rgba(0, 0, 0, 0.4),
|
||||
inset 1px 0 3px rgba(255, 255, 255, 0.2),
|
||||
0 0 12px rgba(91, 30, 140, 0.2);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.wax-drip {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
width: 3px;
|
||||
background: linear-gradient(to bottom, #fcf6ba, #aa771c);
|
||||
border-radius: 0 0 2px 2px;
|
||||
animation: drip-grow 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.wax-drip-1 {
|
||||
left: -2px;
|
||||
height: 12px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.wax-drip-2 {
|
||||
right: -2px;
|
||||
height: 8px;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
@keyframes drip-grow {
|
||||
0% {
|
||||
height: 3px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
60% {
|
||||
height: 14px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
height: 3px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.candle-base {
|
||||
width: 20px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to right, #bf953f, #fcf6ba, #b38728, #aa771c);
|
||||
border-radius: 1px 1px 2px 2px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.candle-scroll-label {
|
||||
margin-top: 6px;
|
||||
font-family: var(--font-script);
|
||||
font-size: 0.8rem;
|
||||
color: var(--brand-gold);
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------
|
||||
ANIMATED HERO SECTION (EMBERS & FOG) STYLES
|
||||
---------------------------------------------------- */
|
||||
.arrival-hero {
|
||||
position: relative;
|
||||
height: 85vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: radial-gradient(circle at center, rgba(212, 162, 76, 0.08), transparent 35%),
|
||||
linear-gradient(135deg, #22112f, #3a1260, #5b1e8c);
|
||||
}
|
||||
|
||||
.arrival-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: radial-gradient(circle at center, transparent 45%, rgba(0, 0, 0, 0.35));
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.arrival-content {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.arrival-copy h1 {
|
||||
text-shadow: 0 10px 40px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.arrival-candle {
|
||||
filter: drop-shadow(0 30px 60px rgba(0, 0, 0, 0.45));
|
||||
transition: filter 2s ease, transform 2s ease;
|
||||
}
|
||||
|
||||
.arrival-glow {
|
||||
position: absolute;
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
border-radius: 50%;
|
||||
filter: blur(90px);
|
||||
z-index: 1;
|
||||
background: radial-gradient(circle, rgba(255, 211, 107, 0.25) 0%, transparent 70%);
|
||||
animation: breathe 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fog Drift Background */
|
||||
.fog {
|
||||
position: absolute;
|
||||
width: 900px;
|
||||
height: 900px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.03), transparent 70%);
|
||||
filter: blur(120px);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fog-1 {
|
||||
top: -300px;
|
||||
left: -200px;
|
||||
animation: drift 30s infinite alternate;
|
||||
}
|
||||
|
||||
.fog-2 {
|
||||
right: -250px;
|
||||
top: -100px;
|
||||
animation: drift 35s infinite alternate-reverse;
|
||||
}
|
||||
|
||||
.fog-3 {
|
||||
bottom: -350px;
|
||||
left: 25%;
|
||||
animation: drift 40s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes drift {
|
||||
0% {
|
||||
transform: translate(0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(120px, 40px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Floating Embers */
|
||||
.embers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.embers span {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-gold-light);
|
||||
box-shadow: 0 0 12px var(--brand-gold-light), 0 0 24px rgba(212, 162, 76, 0.6);
|
||||
animation: ember 12s linear infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes ember {
|
||||
0% {
|
||||
transform: translateY(110vh) translateX(0) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-20vh) translateX(60px) scale(1.2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scroll bounce indicator */
|
||||
.scroll-indicator {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
animation: bounce 2s infinite;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%) translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ticker Announcement Bar override styles */
|
||||
.ticker-bar {
|
||||
background: var(--brand-purple-dark);
|
||||
border-bottom: 1px solid rgba(212, 162, 76, 0.25);
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ticker-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
animation: ticker-scroll 28s linear infinite;
|
||||
white-space: nowrap;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes ticker-scroll {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.ticker-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
padding: 0 2.5rem;
|
||||
}
|
||||
|
||||
.ticker-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-gold);
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Swiper styling overrides for elegant navigation */
|
||||
.swiper-pagination-bullet-active {
|
||||
background: var(--brand-purple) !important;
|
||||
width: 20px !important;
|
||||
border-radius: 4px !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
color: var(--brand-purple) !important;
|
||||
transform: scale(0.6);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.swiper-button-next:hover,
|
||||
.swiper-button-prev:hover {
|
||||
background: var(--brand-purple);
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.swiper-button-next::after,
|
||||
.swiper-button-prev::after {
|
||||
font-size: 20px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Hide Swiper default arrows on mobile */
|
||||
@media (max-width: 600px) {
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './app/App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
// Common Components
|
||||
import Header from '../components/common/Header';
|
||||
import Footer from '../components/common/Footer';
|
||||
import AnnouncementBar from '../components/common/AnnouncementBar';
|
||||
import WishlistDrawer from '../components/common/WishlistDrawer';
|
||||
import CartDrawer from '../components/common/CartDrawer';
|
||||
import WhatsAppButton from '../components/common/WhatsAppButton';
|
||||
import ScrollToTop from '../components/common/ScrollToTop';
|
||||
import CookieConsent from '../components/common/CookieConsent';
|
||||
import NewsletterPopup from '../components/common/NewsletterPopup';
|
||||
import Breadcrumbs from '../components/common/Breadcrumbs';
|
||||
import ScrollCandle from '../components/common/ScrollCandle';
|
||||
|
||||
// Pages
|
||||
import HomePage from '../features/home/pages/HomePage';
|
||||
import ShopPage from '../features/shop/pages/ShopPage';
|
||||
import ProductDetailPage from '../features/shop/pages/ProductDetailPage';
|
||||
import CustomCandlePage from '../features/custom/pages/CustomCandlePage';
|
||||
import CorporateGiftingPage from '../features/b2b/pages/CorporateGiftingPage';
|
||||
import HospitalityPage from '../features/b2b/pages/HospitalityPage';
|
||||
import PrivateLabelPage from '../features/b2b/pages/PrivateLabelPage';
|
||||
import JournalPage from '../features/journal/pages/JournalPage';
|
||||
import ArticleDetailPage from '../features/journal/pages/ArticleDetailPage';
|
||||
import AboutPage from '../features/about/pages/AboutPage';
|
||||
import ContactPage from '../features/contact/pages/ContactPage';
|
||||
|
||||
// Policy Pages
|
||||
import PrivacyPolicyPage from '../features/legal/pages/PrivacyPolicyPage';
|
||||
import TermsConditionsPage from '../features/legal/pages/TermsConditionsPage';
|
||||
import ShippingPolicyPage from '../features/legal/pages/ShippingPolicyPage';
|
||||
import RefundPolicyPage from '../features/legal/pages/RefundPolicyPage';
|
||||
|
||||
interface AppRoutesProps {
|
||||
isB2BMode: boolean;
|
||||
setIsB2BMode: (val: boolean) => void;
|
||||
}
|
||||
|
||||
export const AppRoutes: React.FC<AppRoutesProps> = ({ isB2BMode, setIsB2BMode }) => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
{/* Dynamic Announcement Bar */}
|
||||
<AnnouncementBar />
|
||||
|
||||
{/* Sticky Header with B2C/B2B toggler */}
|
||||
<Header isB2BMode={isB2BMode} setIsB2BMode={setIsB2BMode} />
|
||||
|
||||
{/* Breadcrumb Path Trace */}
|
||||
<Breadcrumbs />
|
||||
|
||||
{/* Main View Area */}
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage isB2BMode={isB2BMode} />} />
|
||||
<Route path="/shop" element={<ShopPage isB2BMode={isB2BMode} />} />
|
||||
<Route path="/shop/:id" element={<ProductDetailPage isB2BMode={isB2BMode} />} />
|
||||
<Route path="/custom-candle" element={<CustomCandlePage isB2BMode={isB2BMode} />} />
|
||||
<Route path="/b2b/corporate-gifting" element={<CorporateGiftingPage />} />
|
||||
<Route path="/b2b/hotels-hospitality" element={<HospitalityPage />} />
|
||||
<Route path="/b2b/private-label" element={<PrivateLabelPage />} />
|
||||
<Route path="/journal" element={<JournalPage />} />
|
||||
<Route path="/journal/:slug" element={<ArticleDetailPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/contact" element={<ContactPage />} />
|
||||
|
||||
{/* Legal policy routes */}
|
||||
<Route path="/privacy-policy" element={<PrivacyPolicyPage />} />
|
||||
<Route path="/terms-conditions" element={<TermsConditionsPage />} />
|
||||
<Route path="/shipping-policy" element={<ShippingPolicyPage />} />
|
||||
<Route path="/refund-policy" element={<RefundPolicyPage />} />
|
||||
|
||||
{/* Fallback */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
|
||||
{/* Brand Footer */}
|
||||
<Footer />
|
||||
|
||||
{/* Sliding Contextual Drawers */}
|
||||
<WishlistDrawer />
|
||||
<CartDrawer />
|
||||
|
||||
{/* Floating Action Elements */}
|
||||
<WhatsAppButton />
|
||||
<ScrollToTop />
|
||||
<ScrollCandle />
|
||||
<CookieConsent />
|
||||
<NewsletterPopup />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#5B1E8C', // Deep Purple
|
||||
light: '#7B3DB8', // Mid Purple
|
||||
dark: '#3A1260', // Dark Purple
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
secondary: {
|
||||
main: '#D4A24C', // Brand Gold
|
||||
light: '#FFD36B', // Light Gold
|
||||
dark: '#A07840', // Dark Gold
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
background: {
|
||||
default: '#EFE4FA', // Light Lavender Background
|
||||
paper: '#FFFFFF',
|
||||
},
|
||||
text: {
|
||||
primary: '#2D0D52', // Deep Purple Text
|
||||
secondary: '#7B5A9E', // Muted Purple-Gray
|
||||
},
|
||||
divider: '#D5BEF0', // Lavender Border
|
||||
},
|
||||
typography: {
|
||||
fontFamily: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`,
|
||||
h1: {
|
||||
fontFamily: `'Cormorant Garamond', Georgia, serif`,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.03em',
|
||||
},
|
||||
h2: {
|
||||
fontFamily: `'Cormorant Garamond', Georgia, serif`,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.02em',
|
||||
},
|
||||
h3: {
|
||||
fontFamily: `'Cormorant Garamond', Georgia, serif`,
|
||||
fontWeight: 400,
|
||||
letterSpacing: '0.02em',
|
||||
},
|
||||
h4: {
|
||||
fontFamily: `'Cormorant Garamond', Georgia, serif`,
|
||||
fontWeight: 400,
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
h5: {
|
||||
fontFamily: `'Cormorant Garamond', Georgia, serif`,
|
||||
fontWeight: 400,
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
h6: {
|
||||
fontFamily: `'Cormorant Garamond', Georgia, serif`,
|
||||
fontWeight: 400,
|
||||
},
|
||||
subtitle1: {
|
||||
fontFamily: `'Inter', sans-serif`,
|
||||
fontWeight: 400,
|
||||
letterSpacing: '0.02em',
|
||||
},
|
||||
body1: {
|
||||
fontFamily: `'Inter', sans-serif`,
|
||||
lineHeight: 1.6,
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
body2: {
|
||||
fontFamily: `'Inter', sans-serif`,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
button: {
|
||||
fontFamily: `'Cinzel', 'Inter', sans-serif`,
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.12em',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '50px', // Premium pill buttons
|
||||
padding: '10px 28px',
|
||||
fontSize: '0.8rem',
|
||||
transition: 'all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
contained: {
|
||||
backgroundColor: '#5B1E8C',
|
||||
color: '#FFFFFF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#3A1260',
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
borderColor: '#5B1E8C',
|
||||
color: '#5B1E8C',
|
||||
'&:hover': {
|
||||
borderColor: '#3A1260',
|
||||
color: '#3A1260',
|
||||
backgroundColor: 'rgba(91, 30, 140, 0.02)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
border: '1px solid #D5BEF0',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.02)',
|
||||
transition: 'all 0.4s ease',
|
||||
'&:hover': {
|
||||
boxShadow: '0 12px 30px rgba(91, 30, 140, 0.08)',
|
||||
transform: 'translateY(-4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: 'rgba(239, 228, 250, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: 'none',
|
||||
borderBottom: '1px solid #D5BEF0',
|
||||
color: '#2D0D52',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
export interface FragranceNotes {
|
||||
top: string[];
|
||||
middle: string[];
|
||||
base: string[];
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
id: string;
|
||||
author: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
date: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
description: string;
|
||||
details: string;
|
||||
price: number; // B2C price
|
||||
b2bPrice: number; // Wholesaler/bulk price per unit (usually for bulk purchases of min qty)
|
||||
minB2BQuantity: number;
|
||||
images: string[];
|
||||
category: 'classic' | 'luxury' | 'seasonal' | 'giftbox';
|
||||
fragranceProfile: 'woody' | 'floral' | 'citrus' | 'oriental' | 'fresh';
|
||||
burnTime: number; // in hours
|
||||
waxType: 'Soy Wax' | 'Coconut Wax' | 'Beeswax' | 'Blend';
|
||||
weight: string; // e.g., "220g / 7.7 oz"
|
||||
fragranceNotes: FragranceNotes;
|
||||
ingredients: string[];
|
||||
careInstructions: string[];
|
||||
rating: number;
|
||||
reviewsCount: number;
|
||||
reviews: Review[];
|
||||
isNew?: boolean;
|
||||
isBestSeller?: boolean;
|
||||
inventory: number;
|
||||
}
|
||||
|
||||
export interface BlogArticle {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
excerpt: string;
|
||||
content: string; // HTML or Markdown content
|
||||
coverImage: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
date: string;
|
||||
author: {
|
||||
name: string;
|
||||
avatar: string;
|
||||
role: string;
|
||||
};
|
||||
readTime: string;
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
product: Product;
|
||||
quantity: number;
|
||||
mode: 'B2C' | 'B2B';
|
||||
customizations?: {
|
||||
wax?: string;
|
||||
jar?: string;
|
||||
scentBlend?: string;
|
||||
labelText?: string;
|
||||
labelFont?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface B2BInquiry {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
companyName: string;
|
||||
website?: string;
|
||||
businessType: 'corporate' | 'hospitality' | 'wedding' | 'retail' | 'interior' | 'other';
|
||||
estimatedQuantity: number;
|
||||
neededBy: string;
|
||||
message: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
import type { Product, BlogArticle } from '../types';
|
||||
|
||||
export const mockProducts: Product[] = [
|
||||
{
|
||||
id: 'sandalwood-saffron',
|
||||
name: 'Sandalwood & Kashmir Saffron',
|
||||
tagline: 'A sacred blend of warm Mysore sandalwood and precious crimson saffron threads.',
|
||||
description: 'Envelop your space in the meditative warmth of royal Mysore sandalwood, paired harmoniously with the delicate, honeyed spice of handpicked Kashmiri saffron. Hand-poured into our signature cream ceramic vessel, this candle represents the pinnacle of Indian luxury craftsmanship.',
|
||||
details: 'This signature fragrance evokes the peaceful atmosphere of ancient Indian temples at dawn. Made from pure soy wax and infused with high-grade natural essential oils, it burns cleanly and disperses scent evenly throughout large spaces.',
|
||||
price: 1899,
|
||||
b2bPrice: 1150,
|
||||
minB2BQuantity: 20,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1602872030219-cbf947a44783?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1596435764223-4d54e48a5bff?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'luxury',
|
||||
fragranceProfile: 'woody',
|
||||
burnTime: 65,
|
||||
waxType: 'Soy Wax',
|
||||
weight: '240g / 8.5 oz',
|
||||
fragranceNotes: {
|
||||
top: ['Kashmiri Saffron', 'Valencia Orange Peel'],
|
||||
middle: ['Mysore Sandalwood', 'Damask Rose Petals'],
|
||||
base: ['Warm Amber', 'Vanilla Bean', 'White Musk']
|
||||
},
|
||||
ingredients: ['100% Natural Soy Wax', 'Therapeutic Grade Essential Oils', 'Organic Cotton Wick'],
|
||||
careInstructions: [
|
||||
'Trim the wick to 1/4 inch before each lighting.',
|
||||
'Allow the wax to melt to the edges of the jar on the first burn to prevent tunneling.',
|
||||
'Do not burn for more than 4 hours at a time.',
|
||||
'Keep away from drafts, children, and pets.'
|
||||
],
|
||||
rating: 4.9,
|
||||
reviewsCount: 42,
|
||||
isBestSeller: true,
|
||||
inventory: 85,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-1',
|
||||
author: 'Aishwarya Sen',
|
||||
rating: 5,
|
||||
comment: 'This candle is absolutely sublime! The scent throw is incredible, filling my living room with a sophisticated, warm sandalwood aroma that feels deeply soothing.',
|
||||
date: '2026-05-12',
|
||||
verified: true
|
||||
},
|
||||
{
|
||||
id: 'rev-2',
|
||||
author: 'Devendra Mehta',
|
||||
rating: 5,
|
||||
comment: 'Bought this as a corporate gift for our top clients and the response was overwhelmingly positive. The ceramic jar is extremely premium.',
|
||||
date: '2026-06-01',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cardamom-oud',
|
||||
name: 'Cardamom & Smoky Oud',
|
||||
tagline: 'An opulent union of green cardamom pods and dark, mysterious agarwood.',
|
||||
description: 'Immerse your senses in the deep, sultry depths of wild Assamese oud, sharpened by the spicy, citrus-fresh complexity of fresh green cardamom. Housed in a matte black glass jar, this candle casts a warm, enigmatic glow and offers an exquisite sensory journey.',
|
||||
details: 'Our Cardamom & Smoky Oud candle is formulated for the connoisseur. The rich, woody base is counter-balanced by exotic spice notes, making it an excellent addition to studies, library rooms, and cozy evenings.',
|
||||
price: 1999,
|
||||
b2bPrice: 1200,
|
||||
minB2BQuantity: 20,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1540555700478-4be289fbecef?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'luxury',
|
||||
fragranceProfile: 'oriental',
|
||||
burnTime: 60,
|
||||
waxType: 'Blend',
|
||||
weight: '230g / 8.1 oz',
|
||||
fragranceNotes: {
|
||||
top: ['Malabar Cardamom', 'Pink Peppercorn'],
|
||||
middle: ['Assamese Oud', 'Cedarwood Shavings'],
|
||||
base: ['Dark Patchouli', 'Leather Accord', 'Tobacco Leaf']
|
||||
},
|
||||
ingredients: ['Natural Soy & Coconut Wax Blend', 'Premium Fragrance Oils', 'Wood Wick'],
|
||||
careInstructions: [
|
||||
'Wood wicks require a short trim (1/8 inch) before each lighting.',
|
||||
'Allow the candle to pool completely to the glass edge to prevent tunneling.',
|
||||
'Extinguish using a snuffer rather than blowing it out.'
|
||||
],
|
||||
rating: 4.8,
|
||||
reviewsCount: 29,
|
||||
isNew: true,
|
||||
inventory: 60,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-3',
|
||||
author: 'Kabir Thapar',
|
||||
rating: 5,
|
||||
comment: 'This is the most authentic oud scent I have found in a candle. The wooden wick crackles softly, creating a relaxing hearth-like ambiance. Simply luxurious.',
|
||||
date: '2026-06-18',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'jasmine-nectar',
|
||||
name: 'Madurai Jasmine & Nectar',
|
||||
tagline: 'A romantic, heady escape featuring fresh night-blooming jasmine flowers.',
|
||||
description: 'Capture the essence of summer nights in Southern India. Fresh, intoxicating Madurai jasmine blossoms are sweetened with a whisper of wild forest nectar and grounded by soft green leaves. Hand-poured in a transparent amber glass jar that diffuses a warm, inviting amber light.',
|
||||
details: 'Highly floral and incredibly uplifting, this candle evokes a sense of pure romance and joy. It is particularly suited for bedrooms, vanity tables, and bathrooms.',
|
||||
price: 1499,
|
||||
b2bPrice: 900,
|
||||
minB2BQuantity: 25,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1590794056226-79ef3a8147e1?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1572635196237-14b3f281503f?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'classic',
|
||||
fragranceProfile: 'floral',
|
||||
burnTime: 50,
|
||||
waxType: 'Soy Wax',
|
||||
weight: '200g / 7.0 oz',
|
||||
fragranceNotes: {
|
||||
top: ['Green Leaves', 'Bergamot Bloom'],
|
||||
middle: ['Madurai Jasmine', 'Mogra Flower', 'Tuberose'],
|
||||
base: ['Forest Honey', 'White Amber', 'Sandalwood']
|
||||
},
|
||||
ingredients: ['100% Organic Soy Wax', 'Pure Flower Extracts', 'Cotton Wick'],
|
||||
careInstructions: [
|
||||
'Trim wick to 1/4 inch before each burn.',
|
||||
'Keep away from drafts to ensure a steady flame.',
|
||||
'Keep the wax pool free of wick trimmings and matches.'
|
||||
],
|
||||
rating: 4.7,
|
||||
reviewsCount: 38,
|
||||
isBestSeller: true,
|
||||
inventory: 110,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-4',
|
||||
author: 'Mira Sen',
|
||||
rating: 4,
|
||||
comment: 'The scent reminds me so much of home. It has that authentic, heavy jasmine aroma without feeling synthetic. Will purchase again.',
|
||||
date: '2026-05-24',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'himalayan-cedar-forest',
|
||||
name: 'Himalayan Cedar & Moss',
|
||||
tagline: 'An earthy, grounding breath of misty alpine woodlands and ancient forest floor.',
|
||||
description: 'Breathe in the crisp, clean air of the high Himalayas. Deep forest moss, resinous blue pine, and majestic deodar cedarwood merge with damp soil notes to create an incredibly grounding and refreshing scent profile. Poured in a heavy, frosted green glass container.',
|
||||
details: 'Designed to mimic the sensory experience of forest bathing, this candle helps clear the mind and reduce stress. It features a sustainable hemp wick that burns steadily.',
|
||||
price: 1599,
|
||||
b2bPrice: 950,
|
||||
minB2BQuantity: 25,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1602872030219-cbf947a44783?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'classic',
|
||||
fragranceProfile: 'fresh',
|
||||
burnTime: 55,
|
||||
waxType: 'Coconut Wax',
|
||||
weight: '210g / 7.4 oz',
|
||||
fragranceNotes: {
|
||||
top: ['Misty Pine Needles', 'Eucalyptus Leaf'],
|
||||
middle: ['Himalayan Deodar Cedar', 'Cypress Wood'],
|
||||
base: ['Oakmoss', 'Wet Earth Patchouli', 'Vetiver']
|
||||
},
|
||||
ingredients: ['100% Biodegradable Coconut Wax', 'High-Altitude Essential Oils', 'Organic Cotton Wick'],
|
||||
careInstructions: [
|
||||
'Keep the wick trimmed to 5mm.',
|
||||
'Ensure the first burn creates a full melt pool to the edges.',
|
||||
'Never leave a burning candle unattended.'
|
||||
],
|
||||
rating: 4.9,
|
||||
reviewsCount: 15,
|
||||
isNew: true,
|
||||
inventory: 40,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-5',
|
||||
author: 'Rohan Deshmukh',
|
||||
rating: 5,
|
||||
comment: 'Very fresh and clean. Unlike many woody candles which can get heavy, this one feels very airy and authentic, like walking through an actual pine forest.',
|
||||
date: '2026-06-25',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chai-cardamom-warmth',
|
||||
name: 'Masala Chai & Ginger',
|
||||
tagline: 'A nostalgic, comforting scent of spiced milk tea, grated ginger, and cinnamon.',
|
||||
description: 'Recreate the warmth of a rainy afternoon with our Masala Chai & Ginger candle. A sweet, spicy blend of crushed cloves, cinnamon bark, cardamom, and fresh ginger root, simmered with black tea leaves and a creamy vanilla milk accord. Poured in a handmade terracotta clay vessel.',
|
||||
details: 'This candle celebrates the ultimate Indian comfort ritual. The terracotta pot can be cleaned and reused for planting or storage after the candle finishes.',
|
||||
price: 1399,
|
||||
b2bPrice: 850,
|
||||
minB2BQuantity: 30,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1576092768241-dec231879fc3?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1605651202774-7d573fd3f12d?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'seasonal',
|
||||
fragranceProfile: 'oriental',
|
||||
burnTime: 45,
|
||||
waxType: 'Soy Wax',
|
||||
weight: '180g / 6.3 oz',
|
||||
fragranceNotes: {
|
||||
top: ['Fresh Ginger', 'Crushed Cloves'],
|
||||
middle: ['Assam Black Tea', 'Malabar Cardamom', 'Cinnamon Bark'],
|
||||
base: ['Steamed Milk Accord', 'Warm Vanilla', 'Nutmeg']
|
||||
},
|
||||
ingredients: ['100% Pure Soy Wax', 'Food-Grade Spices Extract', 'Lead-Free Cotton Wick'],
|
||||
careInstructions: [
|
||||
'Place on a heat-resistant surface as the terracotta pot can get warm.',
|
||||
'Always trim the wick to 1/4 inch.',
|
||||
'Discontinue use when only 1/2 inch of wax remains at the bottom.'
|
||||
],
|
||||
rating: 4.8,
|
||||
reviewsCount: 56,
|
||||
isBestSeller: true,
|
||||
inventory: 95,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-6',
|
||||
author: 'Priya Iyer',
|
||||
rating: 5,
|
||||
comment: 'This smells EXACTLY like brewing hot masala chai on a rainy evening. The terracotta container is so beautiful and unique! Absolutely in love with this brand.',
|
||||
date: '2026-05-30',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'kerala-coconut-lime',
|
||||
name: 'Kerala Coconut & Kaffir Lime',
|
||||
tagline: 'A bright, tropical burst of fresh grated coconut water and zesty lime.',
|
||||
description: 'Evoke the breezy palms of Kerala backwaters. A crisp, energizing splash of local green kaffir lime, infused with the rich, creamy sweetness of freshly pressed coconut milk and a hint of lemongrass. Hand-poured in a sleek frosted glass vessel.',
|
||||
details: 'This fragrance is incredibly refreshing and works perfectly to neutralize odors or brighten up sunlit spaces like kitchens and patios.',
|
||||
price: 1299,
|
||||
b2bPrice: 780,
|
||||
minB2BQuantity: 30,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1540555700478-4be289fbecef?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'seasonal',
|
||||
fragranceProfile: 'citrus',
|
||||
burnTime: 48,
|
||||
waxType: 'Soy Wax',
|
||||
weight: '190g / 6.7 oz',
|
||||
fragranceNotes: {
|
||||
top: ['Kaffir Lime', 'Zesty Bergamot'],
|
||||
middle: ['Fresh Lemongrass', 'Grated Coconut'],
|
||||
base: ['Coconut Water', 'Vanilla Nectar']
|
||||
},
|
||||
ingredients: ['100% Natural Soy Wax', 'Botanical Essential Oils', 'Organic Cotton Wick'],
|
||||
careInstructions: [
|
||||
'Trim wick to 1/4 inch before lighting.',
|
||||
'Burn for at least 2 hours on the first lighting.',
|
||||
'Store in a cool, dry place out of direct sunlight.'
|
||||
],
|
||||
rating: 4.6,
|
||||
reviewsCount: 18,
|
||||
inventory: 70,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-7',
|
||||
author: 'Arjun Nair',
|
||||
rating: 5,
|
||||
comment: 'Very fresh and summer-like. The lime is sharp and clean, and the coconut notes aren\'t overly sweet or heavy. Highly recommend for hot days.',
|
||||
date: '2026-06-11',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'luxury-gift-box',
|
||||
name: 'The Heritage Gift Box',
|
||||
tagline: 'A curated collection of three classic Indian fragrance votives in a luxury hard-box.',
|
||||
description: 'The ultimate luxury gift for weddings, corporate celebrations, or personal indulgences. Contains three medium-sized candles: Sandalwood & Saffron, Madurai Jasmine, and Cardamom & Smoky Oud, elegantly nested in a rigid textured box lined with gold foil details and accompanied by a brass wick trimmer.',
|
||||
details: 'This curated gift set represents the rich tapestry of Indian olfactory heritage. The candles are sized at 120g each (total burn time exceeding 100 hours combined) and are packaged ready for presentation.',
|
||||
price: 3899,
|
||||
b2bPrice: 2400,
|
||||
minB2BQuantity: 10,
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1513201099705-a9746e1e201f?auto=format&fit=crop&q=80&w=800',
|
||||
'https://images.unsplash.com/photo-1549465220-1a8b9238cd48?auto=format&fit=crop&q=80&w=800'
|
||||
],
|
||||
category: 'giftbox',
|
||||
fragranceProfile: 'woody',
|
||||
burnTime: 105,
|
||||
waxType: 'Soy Wax',
|
||||
weight: '360g (3x120g)',
|
||||
fragranceNotes: {
|
||||
top: ['Saffron', 'Malabar Cardamom', 'Green Leaves'],
|
||||
middle: ['Mysore Sandalwood', 'Madurai Jasmine', 'Assamese Oud'],
|
||||
base: ['Warm Amber', 'Forest Honey', 'Tobacco Leaf']
|
||||
},
|
||||
ingredients: ['100% Pure Soy Wax', 'Therapeutic Grade Extracts', '3 Cotton Wicks', 'Solid Brass Wick Trimmer Included'],
|
||||
careInstructions: [
|
||||
'Please read the individual care guide for each candle inside the box.',
|
||||
'Use the included brass wick trimmer to keep wicks short and clean.'
|
||||
],
|
||||
rating: 5.0,
|
||||
reviewsCount: 14,
|
||||
isBestSeller: true,
|
||||
inventory: 50,
|
||||
reviews: [
|
||||
{
|
||||
id: 'rev-8',
|
||||
author: 'Shalini Kapoor',
|
||||
rating: 5,
|
||||
comment: 'This is the most gorgeous gift set I have ever seen. The packaging is extremely heavy and rich. The brass wick trimmer is a beautiful addition. Perfect for wedding favors!',
|
||||
date: '2026-06-29',
|
||||
verified: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const mockArticles: BlogArticle[] = [
|
||||
{
|
||||
id: 'art-1',
|
||||
title: 'The Art of Slow Living: Choosing the Perfect Fragrance for Your Space',
|
||||
slug: 'art-of-slow-living-fragrance-guide',
|
||||
excerpt: 'Explore how different olfactory notes affect mood and discover the ideal candle pairings for your bedroom, living area, and workspace.',
|
||||
content: `
|
||||
<p>In our fast-paced modern lives, creating a sanctuary at home has become more essential than ever. One of the most subtle yet powerful ways to transform an environment is through scent. Olfaction is directly linked to the brain's limbic system, which governs emotions and memory, making your choice of fragrance a vital component of home design.</p>
|
||||
|
||||
<h3>The Living Room: Warmth and Hospitality</h3>
|
||||
<p>Your living area is a communal space meant for gathering and relaxation. Here, you want fragrances that invite conversation and exude warmth. A blend of <strong>Mysore Sandalwood & Kashmiri Saffron</strong> is perfect. The rich, woody base of sandalwood provides grounding, while saffron adds a light, honeyed spice that feels welcoming without overpowering the senses.</p>
|
||||
|
||||
<h3>The Bedroom: Rest and Restoration</h3>
|
||||
<p>For the bedroom, the focus shifts to calming the nervous system and preparing the body for deep rest. Traditional floral notes like <strong>Madurai Jasmine</strong> or herbal combinations like Lavender and Chamomile are ideal. Jasmine, in particular, has been celebrated in Indian culture for centuries for its anxiety-reducing qualities and romantic warmth.</p>
|
||||
|
||||
<h3>The Workspace: Clarity and Focus</h3>
|
||||
<p>When working, you want aromas that stimulate the intellect and clear away mental fatigue. Muted citrus profiles like <strong>kaffir lime, lemongrass, and eucalyptus</strong> are excellent choices. They bring a crisp, clean energy that sharpens focus and keeps you refreshed during long hours at your desk.</p>
|
||||
|
||||
<blockquote style="border-left: 4px solid #C5A880; padding-left: 16px; margin: 24px 0; font-style: italic; color: #5E5852;">
|
||||
"Scent is the invisible architecture of a room. It shapes how we feel, how we interact, and what memories we carry with us long after we leave."
|
||||
</blockquote>
|
||||
|
||||
<h3>How to Burn for Maximum Scentscape</h3>
|
||||
<p>To get the most out of your luxury candles, always trim the wick to 1/4 inch before lighting. Allow the wax to melt completely to the edges of the vessel during the first burn—this creates a "memory pool" that prevents tunneling, ensuring your candle burns evenly and lasts up to 60+ hours.</p>
|
||||
`,
|
||||
coverImage: 'https://images.unsplash.com/photo-1596435764223-4d54e48a5bff?auto=format&fit=crop&q=80&w=800',
|
||||
category: 'Lifestyle',
|
||||
tags: ['Aromatherapy', 'Slow Living', 'Home Decor'],
|
||||
date: 'June 15, 2026',
|
||||
author: {
|
||||
name: 'Aditi Sharma',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=150',
|
||||
role: 'Brand Storyteller & Alchemist'
|
||||
},
|
||||
readTime: '4 min read',
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 'art-2',
|
||||
title: 'Why 100% Pure Soy Wax Matters: The Truth About Paraffin',
|
||||
slug: 'why-soy-wax-matters-clean-burn',
|
||||
excerpt: 'Discover why soy wax is superior to industrial petroleum paraffin, from soot-free burning to ecological sustainability and scent performance.',
|
||||
content: `
|
||||
<p>When buying a candle, most people look at the brand name or smell the fragrance. However, the most critical ingredient is the base material: the wax. The wax dictates how clean the candle burns, how long it lasts, and whether it releases toxic chemicals into your home.</p>
|
||||
|
||||
<h3>What is Paraffin Wax?</h3>
|
||||
<p>Paraffin is a byproduct of the petroleum refining process. It is cheap, easy to scent, and has been the industry standard for decades. However, burning paraffin releases petrochemical soot, which can coat your walls and, worse, find its way into your lungs. It releases chemicals like toluene and benzene, which are known carcinogens.</p>
|
||||
|
||||
<h3>The Soy Wax Difference</h3>
|
||||
<p>Soy wax is derived from natural soybeans. It is a completely renewable, biodegradable resource. Beyond the environmental benefits, soy wax has two main advantages for the user:</p>
|
||||
<ul>
|
||||
<li><strong>Lower Melting Point:</strong> Soy wax melts at a lower temperature than paraffin, which means soy candles burn up to 50% longer than paraffin candles of the same size.</li>
|
||||
<li><strong>Clean Burn:</strong> Soy wax burns soot-free. It does not release toxic chemicals, ensuring your indoor air quality remains pristine.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Our Hand-Poured Process</h3>
|
||||
<p>At Mani Candles, we use only 100% natural, non-GMO soy and coconut waxes. Each candle is poured by hand in small batches in our studio, ensuring the wicks are perfectly centered and the fragrance is distributed evenly throughout the wax. This artisanal focus ensures every burn is as clean, safe, and fragrant as the first.</p>
|
||||
`,
|
||||
coverImage: 'https://images.unsplash.com/photo-1602872030219-cbf947a44783?auto=format&fit=crop&q=80&w=800',
|
||||
category: 'Craftsmanship',
|
||||
tags: ['Sustainability', 'Soy Wax', 'Artisanal'],
|
||||
date: 'May 28, 2026',
|
||||
author: {
|
||||
name: 'Mani Nair',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=150',
|
||||
role: 'Founder & Head Chandler'
|
||||
},
|
||||
readTime: '3 min read'
|
||||
},
|
||||
{
|
||||
id: 'art-3',
|
||||
title: 'The Heritage of Indian Aromatics: A Journey Through Time',
|
||||
slug: 'heritage-of-indian-aromatics',
|
||||
excerpt: 'From ancient Ayurveda to the Mughal perfume capitals, trace the historical roots of sandalwood, vetiver, and jasmine in Indian scent culture.',
|
||||
content: `
|
||||
<p>India is a land steeped in fragrance. From the incense drifting out of temples to the spice markets bustling at noon, aromas are woven into our cultural identity. But our scent heritage is more than just pleasant smells—it is an ancient science and art form that dates back over 5,000 years.</p>
|
||||
|
||||
<h3>Ayurvedic Roots: Healing through Scent</h3>
|
||||
<p>In Ayurvedic scriptures, aromatics are categorized under Gandha (odor). Plants were classified by their thermodynamic effects on the three Doshas (Vata, Pitta, Kapha). Sandalwood (Chandan) was used to cool Pitta, jasmine (Mogra) to soothe the nervous system, and vetiver (Khus) to ground Vata during hot summer months. Aromatherapy was not a trend; it was everyday medicine.</p>
|
||||
|
||||
<h3>Kannauj: The Grasse of the East</h3>
|
||||
<p>During the Mughal era, India developed sophisticated distillation techniques. The town of Kannauj in Uttar Pradesh became world-famous for creating Attars—essential oils distilled into a sandalwood base. Kannauj perfumers created the legendary "Mitti Attar", capturing the exact scent of the first monsoon rain hitting parched soil, a scent known as petrichor.</p>
|
||||
|
||||
<h3>Mani Candles: Preserving Heritage</h3>
|
||||
<p>Our mission is to translate this grand olfactory heritage into modern luxury. By combining traditional ingredients like Mysore Sandalwood, Madurai Jasmine, and Malabar Cardamom with sleek design and sustainable soy wax, we bring the soul of ancient India into modern homes globally.</p>
|
||||
`,
|
||||
coverImage: 'https://images.unsplash.com/photo-1605651202774-7d573fd3f12d?auto=format&fit=crop&q=80&w=800',
|
||||
category: 'Heritage',
|
||||
tags: ['India', 'History', 'Aromatics'],
|
||||
date: 'April 10, 2026',
|
||||
author: {
|
||||
name: 'Aditi Sharma',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=150',
|
||||
role: 'Brand Storyteller & Alchemist'
|
||||
},
|
||||
readTime: '5 min read'
|
||||
}
|
||||
];
|
||||
export const mockFAQs = [
|
||||
{
|
||||
question: "What makes Mani Candles different from typical retail candles?",
|
||||
answer: "Unlike mass-market candles that use petroleum-derived paraffin wax and synthetic chemical fragrances, Mani Candles are hand-poured in India using 100% natural soy and coconut waxes. We use therapeutic-grade essential oils and natural extracts, ensuring a clean, soot-free burn and an authentic, sophisticated olfactory throw. Additionally, all our vessels are designed to be reused."
|
||||
},
|
||||
{
|
||||
question: "Do you offer corporate gifting and bulk order discounts?",
|
||||
answer: "Yes, we specialize in corporate gifting, wedding return gifts, and custom manufacturing. We offer tiered pricing models starting from orders of 20 units or more. You can request a quote directly through our B2B sections or by clicking 'Request Bulk Pricing' on any product page."
|
||||
},
|
||||
{
|
||||
question: "Can I customize the candle labels and fragrances?",
|
||||
answer: "Absolutely. Our 'Custom Candle Builder' allows you to select your preferred wax type, jar finish, and custom scent blend percentages. You can also write a personalized label message and select a font, which we will print and hand-label on your candles. For large B2B orders, we can also customize packaging and engrave vessels."
|
||||
},
|
||||
{
|
||||
question: "What is your shipping policy within India and internationally?",
|
||||
answer: "We offer free standard shipping on all B2C orders above ₹999 across India. B2B and bulk shipping costs are calculated based on weight and volume. We are currently setting up international shipping capabilities; please contact our B2B team for global shipping requests."
|
||||
},
|
||||
{
|
||||
question: "How can I reuse the ceramic and glass candle jars?",
|
||||
answer: "Once your candle has burned down to 1/2 inch of wax, pour hot water into the container. The wax will melt and float to the top. Once cooled, remove the solidified wax disc, wash the container with warm soapy water, and use it as a planter, stationery holder, or cosmetic jar."
|
||||
}
|
||||
];
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
export const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
export const calculateReadTime = (htmlContent: string): string => {
|
||||
const wordsPerMinute = 200;
|
||||
const cleanText = htmlContent.replace(/<\/?[^>]+(>|$)/g, ""); // Strip HTML tags
|
||||
const wordCount = cleanText.split(/\s+/).filter(Boolean).length;
|
||||
const minutes = Math.ceil(wordCount / wordsPerMinute);
|
||||
return `${minutes} min read`;
|
||||
};
|
||||
|
||||
export const slugify = (text: string): string => {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
||||
.replace(/\-\-+/g, '-'); // Replace multiple - with single -
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"allowArbitraryExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"module": "nodenext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||