First Commit

This commit is contained in:
MihirM17 2026-07-05 18:58:13 +05:30
commit ff07c46630
61 changed files with 10141 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -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?

8
.oxlintrc.json Normal file
View File

@ -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 }]
}
}

32
README.md Normal file
View File

@ -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.

18
index.html Normal file
View File

@ -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>

2644
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -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"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@ -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

BIN
public/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

4
src/App.css Normal file
View File

@ -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.
*/

4
src/App.tsx Normal file
View File

@ -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 {};

51
src/app/App.tsx Normal file
View File

@ -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;

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

1
src/assets/react.svg Normal file
View File

@ -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

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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' }}>
&copy; {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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 ecologywhether 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 &rarr;
</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 &rarr;
</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 &rarr;
</Button>
</Paper>
</Grid>
</Grid>
</Container>
</Box>
);
};
export default HomePage;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 builderwhere custom scents are blended, or labels are printed with bespoke textsare 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 &rarr;
</Button>
</Grid>
</Grid>
</DialogContent>
</Dialog>
);
};
export default QuickViewModal;

View File

@ -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;

View File

@ -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;

576
src/index.css Normal file
View File

@ -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;
}
}

10
src/main.tsx Normal file
View File

@ -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>,
)

97
src/routes/AppRoutes.tsx Normal file
View File

@ -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;

142
src/theme/theme.ts Normal file
View File

@ -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;

84
src/types/index.ts Normal file
View File

@ -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;
}

441
src/utils/data.ts Normal file
View File

@ -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 burnthis 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 smellsit 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 Attarsessential 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."
}
];

25
src/utils/helpers.ts Normal file
View File

@ -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 -
};

26
tsconfig.app.json Normal file
View File

@ -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"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

23
tsconfig.node.json Normal file
View File

@ -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"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})