first
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -0,0 +1 @@
|
|||
<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 |
|
@ -0,0 +1,28 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
After Width: | Height: | Size: 735 KiB |
After Width: | Height: | Size: 241 KiB |
After Width: | Height: | Size: 253 KiB |
After Width: | Height: | Size: 664 KiB |
After Width: | Height: | Size: 333 KiB |
After Width: | Height: | Size: 339 KiB |
After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 113 KiB |
After Width: | Height: | Size: 356 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 324 KiB |
After Width: | Height: | Size: 247 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 289 KiB |
After Width: | Height: | Size: 774 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 201 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 545 KiB |
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "healthy-oil",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/cli": "^3.12.0",
|
||||
"@chakra-ui/react": "^3.12.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"add": "^2.0.6",
|
||||
"color-mode": "^0.1.0",
|
||||
"framer-motion": "^12.5.0",
|
||||
"motion": "^12.5.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"snippet": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
|
@ -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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,42 @@
|
|||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import {
|
||||
Box,
|
||||
Container,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
Image,
|
||||
VStack,
|
||||
HStack,
|
||||
SimpleGrid,
|
||||
Center,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import Header from './components/header'
|
||||
import Hero1 from './components/hero1'
|
||||
import Hero2 from './components/hero2'
|
||||
import Compare from './components/compare'
|
||||
import Qa from './components/qa'
|
||||
import Oil_info from './components/oil_info'
|
||||
import Bestoil from './components/bestoil'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Container maxW="100vw" p={0} m={0}>
|
||||
<Header />
|
||||
{/* Hero Section */}
|
||||
<Box position="relative">
|
||||
<Hero1 />
|
||||
<Box mt={{ base: "-5vw", sm: "-5vw", md: "-5vw", lg: "-5vw", xl: "-5vw" }}>
|
||||
<Hero2 />
|
||||
</Box>
|
||||
<Compare />
|
||||
{/* Q&A Section */}
|
||||
<Box mt={{ base: "-15vw", sm: "-15vw", md: "0vw", lg: "0vw", xl: "0vw" }}>
|
||||
<Qa />
|
||||
</Box>
|
||||
<Box>
|
||||
<Oil_info />
|
||||
</Box>
|
||||
<Box>
|
||||
<Bestoil />
|
||||
</Box>
|
||||
</Box>
|
||||
{/* Info Section */}
|
||||
|
||||
|
||||
{/* Footer */}
|
||||
<Box bg="#7BC142" color="white" py={6} w="full">
|
||||
<Box maxW="100%" px={0} mx={0} textAlign="center">
|
||||
<Text mb={2}>服務專線:29437810</Text>
|
||||
<Box w="150px" h="40px" bg="gray.300" borderRadius="md" mx="auto" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
|
@ -0,0 +1,78 @@
|
|||
:root {
|
||||
/* Primary colors */
|
||||
--color-primary-50: #e5f4ea;
|
||||
--color-primary-100: #c1e3cd;
|
||||
--color-primary-200: #9cd2ae;
|
||||
--color-primary-300: #78c18f;
|
||||
--color-primary-400: #54b070;
|
||||
--color-primary-500: #007934; /* Main primary color */
|
||||
--color-primary-600: #006e2f;
|
||||
--color-primary-700: #005e28;
|
||||
--color-primary-800: #004f22;
|
||||
--color-primary-900: #00401b;
|
||||
|
||||
/* Secondary colors (yellow) */
|
||||
--color-secondary-50: #fefae8;
|
||||
--color-secondary-100: #fcf3c4;
|
||||
--color-secondary-200: #fbec9f;
|
||||
--color-secondary-300: #f9e57a;
|
||||
--color-secondary-400: #f8df55;
|
||||
--color-secondary-500: #F7E8A5; /* Main secondary color */
|
||||
--color-secondary-600: #e0d294;
|
||||
--color-secondary-700: #c9bc84;
|
||||
--color-secondary-800: #b2a773;
|
||||
--color-secondary-900: #9c9162;
|
||||
|
||||
/* Accent colors */
|
||||
--color-accent-green: #7BC142;
|
||||
--color-accent-blue: #E3F2FD;
|
||||
--color-accent-orange: #FFF3E0;
|
||||
--color-accent-purple: #F3E5F5;
|
||||
--color-accent-red: #FFEBEE;
|
||||
}
|
||||
|
||||
/* Utility classes for backgrounds */
|
||||
.bg-primary {
|
||||
background-color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--color-secondary-500);
|
||||
}
|
||||
|
||||
.bg-accent-green {
|
||||
background-color: var(--color-accent-green);
|
||||
}
|
||||
|
||||
.bg-accent-blue {
|
||||
background-color: var(--color-accent-blue);
|
||||
}
|
||||
|
||||
.bg-accent-orange {
|
||||
background-color: var(--color-accent-orange);
|
||||
}
|
||||
|
||||
.bg-accent-purple {
|
||||
background-color: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.bg-accent-red {
|
||||
background-color: var(--color-accent-red);
|
||||
}
|
||||
|
||||
/* Utility classes for text colors */
|
||||
.text-primary {
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-secondary-500);
|
||||
}
|
||||
|
||||
.text-accent-green {
|
||||
color: var(--color-accent-green);
|
||||
}
|
||||
|
||||
.text-normalGreen{
|
||||
color: "#075C39",
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export const colors = {
|
||||
textColor: '#075C39',
|
||||
backgroundColor: '#FFFBCE',
|
||||
topBarColor: '#92C000',
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import { Box, Stack, Image, Flex, useBreakpointValue, Text, SimpleGrid } from '@chakra-ui/react'
|
||||
import { colors } from '../colors';
|
||||
|
||||
function Bestoil() {
|
||||
const cook = useBreakpointValue({
|
||||
base: "/images/cook_mb.png",
|
||||
sm: "/images/cook_mb.png",
|
||||
md: "/images/cook_pc.png",
|
||||
|
||||
});
|
||||
const oilCube = [
|
||||
{
|
||||
bgColor: 'linear-gradient(to right,#00609E ,#008FD0,#00609E )',
|
||||
title: '米糠油',
|
||||
text: '含天然谷維素,具抗氧化作用,有助維持血管健康'
|
||||
},
|
||||
{
|
||||
bgColor: 'linear-gradient(to right,#D44E2D ,#EAA72E,#D44E2D )',
|
||||
title: '芥花籽油',
|
||||
text: '富含多元不飽和脂肪Omega-3、6,有助降低心血管疾病風險'
|
||||
},
|
||||
{
|
||||
bgColor: 'linear-gradient(to right,#7DAE3A ,#AFC226,#7DAE3A )',
|
||||
title: '橄欖油',
|
||||
text: '富含單元不飽和脂肪酸Omega-9,有助保護心臟健康'
|
||||
},
|
||||
{
|
||||
bgColor: 'linear-gradient(to right,#0098D2 ,#00AFE6,#0098D2 )',
|
||||
title: '亞麻籽油',
|
||||
text: '是亞麻酸(Omega-3) 的良好來源,有助維持細胞健康'
|
||||
},
|
||||
{
|
||||
bgColor: 'linear-gradient(to right,#D23431 ,#E28C24,#D23431 )',
|
||||
title: '高油酸葵花籽油',
|
||||
text: '低飽和脂肪,穩定性高,適合高溫烹調'
|
||||
},
|
||||
]
|
||||
const formatText = (text: string) => {
|
||||
return text.split('\n').map((line, i) => (
|
||||
<Box key={i}
|
||||
color={'white'}
|
||||
fontSize='9px'
|
||||
mt={-2}
|
||||
className='font-noto-sans font-regular'
|
||||
>
|
||||
{line.split('\t').map((segment, j) => (
|
||||
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||
))}
|
||||
{i < text.split('\n').length - 1 && <br />}
|
||||
</Box>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
position="relative"
|
||||
w="100%"
|
||||
overflow="hidden"
|
||||
|
||||
bgColor={colors.backgroundColor}
|
||||
>
|
||||
|
||||
<Flex
|
||||
w='100%'
|
||||
justify={"center"}
|
||||
>
|
||||
<Image src="/images/best5.png"
|
||||
fit='contain'
|
||||
width={'500px'} />
|
||||
<Image src="/images/oilchart.png"
|
||||
fit='contain'
|
||||
width={'500px'} />
|
||||
|
||||
</Flex>
|
||||
|
||||
<Stack
|
||||
w='100%'
|
||||
align={"center"}
|
||||
|
||||
>
|
||||
<Image src={cook}
|
||||
w={{ base: "100%", sm: "100%", md: '600px' }}
|
||||
fit='contain'
|
||||
/>
|
||||
|
||||
<Text
|
||||
w={{ base: "90%", sm: "90%", md: '600px' }}
|
||||
fontSize={'lg'}
|
||||
className='NotoSansCJKtc font-regular'
|
||||
color={colors.textColor}
|
||||
textAlign={"center"}>
|
||||
{"不同食油的脂肪酸組合各有優勢,而長期使用單一油種則可能令營養失衡。營萃護心油融合5種優質食油,發揮更全面的健康效益:"}
|
||||
</Text>
|
||||
|
||||
|
||||
<Box width="100%" display="flex" flexDirection="column" alignItems="center" mt={5}>
|
||||
<Flex flexWrap="wrap" justifyContent="center" gap="15px" maxWidth="945px">
|
||||
{oilCube.map((item, index) => (
|
||||
<Stack
|
||||
key={index}
|
||||
w='270px'
|
||||
h='90px'
|
||||
bgImage={item.bgColor}
|
||||
roundedTopLeft={'30px'}
|
||||
roundedBottomRight={'30px'}
|
||||
p={4}
|
||||
justify="center"
|
||||
>
|
||||
<Text
|
||||
|
||||
color="white"
|
||||
className='font-melle font-black'
|
||||
fontSize="2xl"
|
||||
mb={-1}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text
|
||||
|
||||
color={colors.backgroundColor}
|
||||
className='font-melle font-medium'
|
||||
mt={-1}
|
||||
>
|
||||
{item.text}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
|
||||
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</Stack>
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default Bestoil
|
|
@ -0,0 +1,93 @@
|
|||
import { Box, Stack, Image, Text, Flex, SimpleGrid } from '@chakra-ui/react'
|
||||
import { colors } from '../colors';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
|
||||
// Create motion versions of Chakra components
|
||||
const MotionStack = motion(Stack);
|
||||
const MotionImage = motion(Image);
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
function Compare() {
|
||||
const compareRef = useRef(null);
|
||||
const isInView = useInView(compareRef, { once: true, amount: 0.3 });
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={compareRef}
|
||||
w="100%"
|
||||
position="relative"
|
||||
>
|
||||
<SimpleGrid columns={2}
|
||||
maxW="full"
|
||||
h={{ base: '140vw', sm: '140vw', md: '120vw', lg: '50vw', xl: '55vw' }}
|
||||
mb="20px" // Add 20px bottom margin
|
||||
position="relative"
|
||||
zIndex="1"
|
||||
>
|
||||
<Stack
|
||||
position="relative"
|
||||
w="100%"
|
||||
h="100%"
|
||||
bgImage={`
|
||||
linear-gradient(to bottom,
|
||||
rgba(255, 251, 210, 1) 4%,
|
||||
rgba(255, 251, 210, 0.1) 10%),
|
||||
url(/images/bgyellow.jpg)
|
||||
`}
|
||||
bgSize="cover"
|
||||
bgPos="center"
|
||||
|
||||
>
|
||||
<Box bgColor="rgba(255, 251, 210, 0.4)" w="100%" h="100%" />
|
||||
{/* Content container with z-index to appear above the background */}
|
||||
</Stack>
|
||||
<Stack
|
||||
w="100%"
|
||||
h="100%"
|
||||
bgImage="linear-gradient(to bottom, #FFFBD2 4%, #E1E1E2 10%)"
|
||||
>
|
||||
{/* Content here */}
|
||||
<MotionImage
|
||||
src='/images/conpareheart.png'
|
||||
width={{
|
||||
base: '95%', sm: '90% ', md: '80% ', lg: '35% '
|
||||
}}
|
||||
position="absolute"
|
||||
top="0%"
|
||||
left="50%"
|
||||
transform="translateX(-51%)"
|
||||
zIndex="1"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5 }}
|
||||
/>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Use a larger negative margin to pull the flex up into the grid area */}
|
||||
<Flex
|
||||
direction="column"
|
||||
justify="center"
|
||||
align="center"
|
||||
width="100%"
|
||||
mt={{ base: '-25vw', sm: '-25vw', md: '-20vw', lg: '-10vw' }}
|
||||
position="relative"
|
||||
zIndex="2"
|
||||
>
|
||||
<MotionImage
|
||||
src="/images/bigheart.png"
|
||||
width={{
|
||||
base: '100%%', sm: '95% ', md: '80% ', lg: '35% '
|
||||
}}
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5 }}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export default Compare;
|
|
@ -0,0 +1,17 @@
|
|||
import { Box, Flex, Image, Text, } from '@chakra-ui/react';
|
||||
import {colors} from '../colors';
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<Box bg={colors.topBarColor} py={4} w="full">
|
||||
<Flex alignItems={'center'} justifyContent={'center'} direction="column">
|
||||
<Image src="/images/headerlogo.png" alt="Logo" width="150px" />
|
||||
<Text textAlign={'center'} marginLeft={3} marginTop={2} className="font-melle font-medium" color={'#075C39'}>
|
||||
{"【積極求變 健康向前】"}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
|
@ -0,0 +1,116 @@
|
|||
import { Box, Stack, SimpleGrid, Image, Image as ChakraImage, Flex, useBreakpointValue } from '@chakra-ui/react'
|
||||
import { motion } from 'framer-motion'
|
||||
function Hero1() {
|
||||
const MotionImage = motion(ChakraImage);
|
||||
const oilImage = useBreakpointValue({
|
||||
base: "/images/oilmobile.png",
|
||||
sm: "/images/oil.png",
|
||||
|
||||
});
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
w="100%"
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
overflow="hidden"
|
||||
|
||||
>
|
||||
<Box
|
||||
overflow="hidden"
|
||||
position="absolute"
|
||||
width="100%"
|
||||
height="100%"
|
||||
//top={{ base: "-3%", sm: "-3%" }}
|
||||
left="0"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
pointerEvents="none"
|
||||
zIndex="1"
|
||||
>
|
||||
<MotionImage
|
||||
src={oilImage}
|
||||
objectFit="contain"
|
||||
marginTop={{ base: "30vw", sm: "5vw", md: "5vw", lg: "0vw", xl: "0vw" }}
|
||||
marginRight={{ base: "20vw", sm: "0vw", md: "0vw", lg: "0vw", xl: "0vw" }}
|
||||
height={{ base: "85vw", sm: "45vw", md: "32vw", lg: "25vw", xl: "25vw" }}
|
||||
maxW="100%" // Prevents the image from being too wide
|
||||
// Adjust vertical position as needed
|
||||
pointerEvents="none"
|
||||
initial={{ opacity: 0, x: -100 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap={0} >
|
||||
<Box
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
w="100%"
|
||||
bgImage={"url('/images/background.png')"}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
bgRepeat="no-repeat"
|
||||
h={{ base: "125vw", sm: "70vw", md: "50vw", lg: "40vw", xl: "35vw" }}
|
||||
|
||||
>
|
||||
<SimpleGrid columns={{ base: 1, sm: 2, md: 2, lg: 2, xl: 2 }} marginTop={10} >
|
||||
<Flex
|
||||
justify={{ base: "center", sm: "flex-end", md: "flex-end", lg: "flex-end", xl: "flex-end" }}
|
||||
align={"flex-start"}
|
||||
h="fit-content" // or a specific height
|
||||
alignSelf="flex-start" // This constrains the Flex to its content height
|
||||
>
|
||||
<MotionImage src="/images/text1.png"
|
||||
width={{ base: "90%", sm: "80%", md: "70%", lg: "70%", xl: "60%" }}
|
||||
fit="contain"
|
||||
h="auto"
|
||||
marginRight={{ sm: "-10%", md: "-10%", lg: "-10%", xl: "-10%" }}
|
||||
marginTop={{ base: 0, sm: 5, md: 5, lg: 5, xl: 5 }}
|
||||
initial={{ opacity: 0, x: -100 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }} />
|
||||
|
||||
</Flex>
|
||||
<Flex
|
||||
|
||||
overflow="hidden"
|
||||
h="auto"
|
||||
alignItems={{ base: "flex-end", sm: "flex-start", md: "flex-start", lg: "flex-start", xl: "flex-start" }}
|
||||
justifyContent={{ base: "center", sm: "flex-start", md: "flex-start", lg: "flex-start", xl: "flex-start" }}
|
||||
>
|
||||
<MotionImage
|
||||
src="/images/people2.png"
|
||||
h={{ base: "120vw", sm: "70vw", md: "50vw", lg: "50vw", xl: "50vw" }}
|
||||
// maxH={"50%"}
|
||||
marginRight={{ base: "-30%", sm: "0", md: "0", lg: "0", xl: "0" }}
|
||||
objectFit="cover"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</SimpleGrid>
|
||||
|
||||
</Box>
|
||||
<Box
|
||||
position="relative"
|
||||
h={{ base: "30vw", sm: "30vw", md: "30vw", lg: "30vw", xl: "20vw" }}
|
||||
w="100%"
|
||||
bgImage={"url('/images/woodtable.png')"}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
justifyContent={"center"}
|
||||
alignSelf={"center"}
|
||||
bgRepeat="no-repeat"
|
||||
|
||||
>
|
||||
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
</Box>
|
||||
);
|
||||
} export default Hero1;
|
|
@ -0,0 +1,197 @@
|
|||
import { Box, Stack, Image, Text, Flex, SimpleGrid } from '@chakra-ui/react'
|
||||
import { colors } from '../colors';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, useInView, useAnimation } from 'framer-motion'
|
||||
|
||||
// Create motion components from Chakra UI components
|
||||
const MotionBox = motion(Box);
|
||||
const MotionFlex = motion(Flex);
|
||||
const MotionImage = motion(Image);
|
||||
const MotionText = motion(Text);
|
||||
const MotionStack = motion(Stack);
|
||||
|
||||
function Hero2() {
|
||||
const textStackRef = useRef<HTMLDivElement>(null);
|
||||
const imageStackRef = useRef<HTMLDivElement>(null);
|
||||
const [textStackHeight, setTextStackHeight] = useState<number | null>(null);
|
||||
|
||||
// Create refs and animation controls for scroll-based animations
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(containerRef, { once: true, amount: 0.2 });
|
||||
const textControls = useAnimation();
|
||||
const imageControls = useAnimation();
|
||||
|
||||
// Trigger animations when component comes into view
|
||||
useEffect(() => {
|
||||
if (isInView) {
|
||||
textControls.start("visible");
|
||||
imageControls.start("visible");
|
||||
} else {
|
||||
textControls.start("hidden");
|
||||
imageControls.start("hidden");
|
||||
}
|
||||
}, [isInView, textControls, imageControls]);
|
||||
|
||||
// Add this useEffect to measure and update heights
|
||||
useEffect(() => {
|
||||
const updateHeight = () => {
|
||||
if (textStackRef.current) {
|
||||
const height = textStackRef.current.offsetHeight;
|
||||
setTextStackHeight(height);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial measurement
|
||||
updateHeight();
|
||||
|
||||
// Setup resize observer for responsive adjustments
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
if (textStackRef.current) {
|
||||
resizeObserver.observe(textStackRef.current);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (textStackRef.current) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
w="100%"
|
||||
position="relative"
|
||||
zIndex="1"
|
||||
bgColor={colors.backgroundColor}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
justify={"flex-start"}
|
||||
align={"center"}
|
||||
>
|
||||
<Image
|
||||
src="/images/title1.png"
|
||||
w={{base:"420px",sm:"450px",md:"450px",lg:"350px",xl:"350px"}}
|
||||
maxW={"95%"}
|
||||
/>
|
||||
<SimpleGrid
|
||||
gap="25px"
|
||||
columns={{ base: 1, md: 1, lg: 2, xl: 2 }}
|
||||
px="4"
|
||||
maxW="full"
|
||||
mx="0"
|
||||
marginTop={{ base: -7, md: -3, lg: 0, xl: 0 }}
|
||||
height={{ base: "auto", md: "auto", lg: "auto", xl: "25vw", '2xl': "15vw" }}
|
||||
>
|
||||
<Flex justify="flex-end">
|
||||
<Stack
|
||||
id='image_stack'
|
||||
ref={imageStackRef}
|
||||
alignItems='flex-end'
|
||||
display={{ base: "none", md: "none", lg: "flex" }}
|
||||
w={{ md: "80%", lg: "70%", xl: "60%" }}
|
||||
h={"auto"}
|
||||
>
|
||||
<MotionImage
|
||||
src="/images/oldman.jpg"
|
||||
rounded="4xl"
|
||||
marginTop={'10px'}
|
||||
height={{ md: "100%", lg: "85%", xl: "65%" }}
|
||||
fit="cover"
|
||||
objectPosition="center"
|
||||
variants={{
|
||||
hidden: { scale: 0.95, opacity: 0 },
|
||||
visible: { scale: 1, opacity: 1 }
|
||||
}}
|
||||
initial="hidden"
|
||||
animate={imageControls}
|
||||
transition={{ duration: 0.7, delay: 0.4 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
/>
|
||||
</Stack>
|
||||
</Flex>
|
||||
|
||||
<Flex justify={{ sm: 'center', md: "center", lg: "flex-start" }}>
|
||||
<Stack
|
||||
w={{ sm: "100%", md: "80%", lg: "90%", xl: "60%" }}
|
||||
h="auto"
|
||||
id="text_stack"
|
||||
ref={textStackRef}
|
||||
align={{ sm: 'flex-start', md: "flex-start", lg: "flex-start" }}
|
||||
>
|
||||
<Flex direction={"column"}>
|
||||
<MotionText
|
||||
color={colors.textColor}
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: "2xl", sm: "3xl", md: "3xl", lg: "3xl" }}
|
||||
variants={{
|
||||
hidden: { x: -10, opacity: 0 },
|
||||
visible: { x: 0, opacity: 1 }
|
||||
}}
|
||||
initial="hidden"
|
||||
animate={textControls}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
>
|
||||
<strong>外出用餐</strong>難控用油,
|
||||
</MotionText>
|
||||
<MotionText
|
||||
color={colors.textColor}
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: "2xl", sm: "3xl", md: "3xl", lg: "3xl" }}
|
||||
marginTop={-3}
|
||||
variants={{
|
||||
hidden: { x: -10, opacity: 0 },
|
||||
visible: { x: 0, opacity: 1 }
|
||||
}}
|
||||
initial="hidden"
|
||||
animate={textControls}
|
||||
transition={{ duration: 0.5, delay: 0.7 }}
|
||||
>
|
||||
不健康油脂或增<strong>「三高」</strong>風險
|
||||
</MotionText>
|
||||
</Flex>
|
||||
<MotionImage
|
||||
src="/images/oldman.jpg"
|
||||
rounded="4xl"
|
||||
marginTop={'3px'}
|
||||
h={{ md: "90%", lg: "65%", xl: "65%" }}
|
||||
display={{ base: "flex", md: "flex", lg: "none" }}
|
||||
fit="cover"
|
||||
objectPosition="center"
|
||||
variants={{
|
||||
hidden: { scale: 0.95, opacity: 0 },
|
||||
visible: { scale: 1, opacity: 1 }
|
||||
}}
|
||||
initial="hidden"
|
||||
animate={imageControls}
|
||||
transition={{ duration: 0.7, delay: 0.4 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
/>
|
||||
|
||||
<MotionText
|
||||
color={colors.textColor}
|
||||
className="font-noto-sans font-bold"
|
||||
lineHeight={2}
|
||||
variants={{
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: { y: 0, opacity: 1 }
|
||||
}}
|
||||
initial="hidden"
|
||||
animate={textControls}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
>
|
||||
{"都市人難免外出用餐,即使精選食材,也無法控制下鍋的食油,一天吃進多少隱形壞油難以估算。在家煮食雖可自選用油,但你確定自己用的是「好油」嗎?長期攝取高飽和脂肪、反式脂肪(如牛油、豬油、加工食品、重複使用的煎炸油),會提升壞膽固醇,導致血管堵塞,增加高血壓、高血脂及心血管疾病風險。"}
|
||||
</MotionText>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Hero2;
|
|
@ -0,0 +1,236 @@
|
|||
import { Box, Stack, Image, Flex, useBreakpointValue, Text } from '@chakra-ui/react'
|
||||
import { motion, useInView, useAnimation } from 'framer-motion'
|
||||
import { useRef, useEffect } from 'react'
|
||||
const MotionFlex = motion(Flex);
|
||||
function Oil_info() {
|
||||
const oilinfotitle = useBreakpointValue({
|
||||
base: "/images/mboilinfotitle.png",
|
||||
sm: "/images/mboilinfotitle.png",
|
||||
md: "/images/oilinfotitle.png",
|
||||
});
|
||||
|
||||
const oilinfogroup = useBreakpointValue({
|
||||
base: "/images/mboilinfogroup.png",
|
||||
sm: "/images/mboilinfogroup.png",
|
||||
md: "/images/pcoilinfogroup.png",
|
||||
});
|
||||
|
||||
// Animation controls for the header section
|
||||
const headerControls = useAnimation();
|
||||
const headerRef = useRef(null);
|
||||
const headerInView = useInView(headerRef, { once: true, amount: 0.3 });
|
||||
|
||||
// Animation controls for the footer group image
|
||||
const footerControls = useAnimation();
|
||||
const footerRef = useRef(null);
|
||||
const footerInView = useInView(footerRef, { once: true, amount: 0.3 });
|
||||
|
||||
// Trigger animations when elements come into view
|
||||
useEffect(() => {
|
||||
if (headerInView) {
|
||||
headerControls.start({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.8, ease: "easeOut" }
|
||||
});
|
||||
}
|
||||
}, [headerInView, headerControls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (footerInView) {
|
||||
footerControls.start({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.8, ease: "easeOut" }
|
||||
});
|
||||
}
|
||||
}, [footerInView, footerControls]);
|
||||
|
||||
const formatText = (text: string) => {
|
||||
return text.split('\n').map((line, i) => (
|
||||
<Box key={i}
|
||||
color={'white'}
|
||||
fontSize='9px'
|
||||
mt={-2}
|
||||
className='font-noto-sans font-regular'
|
||||
>
|
||||
{line.split('\t').map((segment, j) => (
|
||||
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||
))}
|
||||
{i < text.split('\n').length - 1 && <br />}
|
||||
</Box>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
position="relative"
|
||||
w="100%"
|
||||
overflow="hidden"
|
||||
bgColor={'#4E8C34'}
|
||||
>
|
||||
<Stack>
|
||||
<MotionFlex
|
||||
as={motion.div}
|
||||
ref={headerRef}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={headerControls}
|
||||
direction="column"
|
||||
bgImage={"url('/images/oilinfobg.png')"}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
bgRepeat="no-repeat"
|
||||
w='100%'
|
||||
h='300px'
|
||||
>
|
||||
<Flex
|
||||
mt={{ base: 10, sm: 10, md: 10 }}
|
||||
justify={"center"}
|
||||
align={"center"}
|
||||
>
|
||||
<Stack
|
||||
w={{ base: '75%', sm: '75%', md: '300px' }}
|
||||
h='auto'
|
||||
alignItems={"center"}
|
||||
justify={"center"}
|
||||
align={"center"}
|
||||
>
|
||||
<Image src="/images/oilinfoheadertitle.png"
|
||||
fit='contain'
|
||||
w='100%'
|
||||
h='auto'
|
||||
></Image>
|
||||
</Stack>
|
||||
<Stack
|
||||
display={{ base: 'none', sm: 'none', md: 'flex' }}
|
||||
w='500px'
|
||||
align={"flex-end"}
|
||||
justify={"center"}
|
||||
>
|
||||
<Text
|
||||
color='white'
|
||||
className='NotoSansCJKtc font-regular'
|
||||
mb={10}
|
||||
lineHeight={1.5}
|
||||
fontSize={'lg'}
|
||||
>
|
||||
{"市場上食油選擇繁多,卻缺少一款真正符合人體對脂肪酸需求的「黃金比例」食油。長期依賴單一油種(如橄欖油、葵花籽油),可能導致脂肪酸及 Omega-3、6、9 攝取失衡。研究顯示,當人體吸收過多Omega-6,發炎風險可增 3 倍,無形中加重心血管負擔。"}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</MotionFlex>
|
||||
</Stack>
|
||||
|
||||
{/* Rest of the component remains the same until the footer image */}
|
||||
|
||||
<Stack
|
||||
display={{ base: 'flex', sm: 'flex', md: 'none' }}
|
||||
mt={{ base: "-40px", sm: 0, md: 0 }}
|
||||
w="100%"
|
||||
align={"center"}
|
||||
>
|
||||
<Text
|
||||
w="80%"
|
||||
color='white'
|
||||
className='NotoSansCJKtc font-regular'
|
||||
lineHeight={1.5}
|
||||
fontSize={'lg'}
|
||||
>
|
||||
{"市場上食油選擇繁多,卻缺少一款真正符合人體對脂肪酸需求的「黃金比例」食油。長期依賴單一油種(如橄欖油、葵花籽油),可能導致脂肪酸及 Omega-3、6、9 攝取失衡。研究顯示,當人體吸收過多Omega-6,發炎風險可增 3 倍,無形中加重心血管負擔。"}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
w='100%'
|
||||
gap={0}
|
||||
align='center'
|
||||
>
|
||||
<Flex
|
||||
w={{ base: '80%', sm: '80%', md: "100%" }}
|
||||
justify={{ base: 'flex-start', sm: 'flex-start', md: 'center' }}
|
||||
>
|
||||
<Image src={oilinfotitle}
|
||||
w='400px'
|
||||
mt={{ base: 2, sm: 2, md: -12 }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
mt={{ base: 0, sm: 0, md: "-60px" }}
|
||||
direction={{ base: 'column', sm: 'column', md: 'row' }}
|
||||
gap={3}
|
||||
align={"center"}
|
||||
>
|
||||
<Image src="/images/oilinfooil.png"
|
||||
w={{ base: '70%', sm: '80%', md: '350px' }}
|
||||
/>
|
||||
<Stack
|
||||
justify={"center"}
|
||||
w={{ base: '80%', sm: '80%', md: '500px' }}
|
||||
>
|
||||
<Stack
|
||||
display={{ base: 'flex', sm: 'flex', md: 'none' }}
|
||||
>
|
||||
{formatText(`*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||
^根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算`)}
|
||||
</Stack>
|
||||
|
||||
<Text
|
||||
color='white'
|
||||
className='NotoSansCJKtc font-medium'
|
||||
mb={10}
|
||||
lineHeight={1.5}
|
||||
fontSize={'lg'}
|
||||
>
|
||||
<Box as="span" className='NotoSansCJKtc font-xbold' display="inline">世界衛生組織建議,</Box>
|
||||
{"人體應攝取良好的脂肪酸比例,這可由食油中攝取,從而調節膽固醇,保護心血管健康。"}
|
||||
</Text>
|
||||
<Text
|
||||
color='white'
|
||||
className='NotoSansCJKtc font-medium'
|
||||
mb={10}
|
||||
lineHeight={1.5}
|
||||
fontSize={'lg'}
|
||||
>
|
||||
{"獅球嘜「營萃護心油」採用黃金比例調配,讓每一滴都成為心血管的隱形防護網。"}
|
||||
</Text>
|
||||
<Stack
|
||||
display={{ base: 'none', sm: 'none', md: 'flex' }}
|
||||
mt={-5}>
|
||||
{formatText(`*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||
^根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算`)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Stack>
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
bgImage={"url('/images/oilinfofooterbg.png')"}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
bgRepeat="no-repeat"
|
||||
w='100%'
|
||||
h='auto'
|
||||
>
|
||||
<Stack
|
||||
w='100%'
|
||||
mb={10}
|
||||
align={"center"}
|
||||
ref={footerRef}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={footerControls}
|
||||
>
|
||||
<Image
|
||||
src={oilinfogroup}
|
||||
w={{ base: "80%", sm: "80%", md: '850px' }}
|
||||
/>
|
||||
</motion.div>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
export default Oil_info;
|
|
@ -0,0 +1,152 @@
|
|||
import { Box, Image, Text, Flex, Accordion } from '@chakra-ui/react'
|
||||
import { colors } from '../colors';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
const MotionBox = motion(Box);
|
||||
const questions = [
|
||||
{
|
||||
image: '/images/q1.png',
|
||||
question: "煮餸落少啲油,甚至唔落油就等於健康?",
|
||||
answer: "錯!油脂是身體合成荷爾蒙的重要材料,若完全不攝取油脂,可能會導致皮膚變乾、容易脫髮,對女生來說更可能影響生理週期。簡單來說,身體需要「好脂肪」來幫助吸收營養,適量攝取優質油脂更有助維持細胞健康!"
|
||||
},
|
||||
{
|
||||
image: '/images/q2.png',
|
||||
question: "用橄欖油煮食就一定最健康?",
|
||||
answer: "未必!橄欖油雖富含單元不飽和脂肪酸如Omega-9,但人體可自行合成,而其 Omega-3 和 Omega-6 含量偏低,僅約 1% 和 7%,脂肪酸比例並不均衡,對小孩及孕婦來說,長期單一使用可能無法滿足他們對營養素的需求。而初榨橄欖油煙點低(190°C),高溫煮食容易產生有害物質,反成健康負擔。"
|
||||
},
|
||||
{
|
||||
image: '/images/q3.png',
|
||||
question: "使用單一油種的食用油較好?",
|
||||
answer: "未必!橄欖油雖富含單元不飽和脂肪酸如Omega-9,但人體可自行合成,而其Omega-3和 Omega-6 含量偏低,脂肪酸比例並不均衡,對小孩及孕婦來說,長期單一使用可能無法滿足他們對營養素的需求。而初榨橄欖油煙點較低(約190°C),高溫煮食容易產生有害物質,反成健康負擔。"
|
||||
},
|
||||
{
|
||||
image: '/images/q4.png',
|
||||
question: "貴價油 = 健康油?",
|
||||
answer: `健康關鍵在於「脂肪酸比例」,而非價格!
|
||||
不同脂肪酸對健康影響各異:
|
||||
\t• 單元不飽和脂肪酸(MUFA / 例如Omega-9) - 有助降低壞膽固醇
|
||||
\t• 多元不飽和脂肪酸(PUFA / 例如Omega-3、6) - 需攝取適當比例,Omega-6過多會引發炎症
|
||||
\t• 飽和脂肪(SFA) - 過量攝取會提升壞膽固醇,影響心血管健康`
|
||||
},
|
||||
{
|
||||
image: '/images/q5.png',
|
||||
question: "食油種類太多,點揀先唔會中伏?",
|
||||
answer: `市面上的食油種類繁多,從花生油、粟米油、芥花籽油、橄欖油、米糠油到牛油果油,每種油都有不同的營養價值和適用範圍。要選擇真正健康和適合日常使用的食油,只需記住 3 個關鍵原則:
|
||||
|
||||
看煙點:高溫煎炸選用煙點高於200°C的食油,可減少有害物質產生
|
||||
看脂肪酸比例:單元不飽和脂肪酸(MUFA)與多元不飽和脂肪酸(PUFA)比例,確保均衡攝取營養
|
||||
看認證:符合國際營養機構推薦,如世界衛生組織(WHO)建議的脂肪酸攝取比例`
|
||||
}
|
||||
]
|
||||
|
||||
function Qa() {
|
||||
const formatText = (text: string) => {
|
||||
return text.split('\n').map((line, i) => (
|
||||
<Box key={i}
|
||||
color={colors.textColor}
|
||||
fontSize='lg'
|
||||
ml={1}
|
||||
className='font-noto-sans font-regular'
|
||||
>
|
||||
{line.split('\t').map((segment, j) => (
|
||||
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||
))}
|
||||
{i < text.split('\n').length - 1 && <br />}
|
||||
</Box>
|
||||
));
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
w="100%"
|
||||
position="relative"
|
||||
>
|
||||
<Accordion.Root variant="plain" multiple={true} >
|
||||
{questions.map((item, index) => {
|
||||
// Create a ref for each question item
|
||||
const questionRef = useRef(null);
|
||||
// Use useInView with once:true to trigger animation only once when scrolled into view
|
||||
const isInView = useInView(questionRef, {
|
||||
once: true, // Only trigger once
|
||||
amount: 0.3 // Trigger when 30% of element is in viewport
|
||||
});
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
ref={questionRef}
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
delay: index * 0.15,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
<Accordion.Item value={item.question} width={'full'} bgColor='white'>
|
||||
<Accordion.ItemTrigger
|
||||
_hover={{
|
||||
boxShadow: 'none',
|
||||
outline: 'none',
|
||||
border: 'none'
|
||||
}}
|
||||
_focus={{
|
||||
boxShadow: 'none',
|
||||
outline: 'none',
|
||||
border: 'none'
|
||||
}}
|
||||
_active={{
|
||||
boxShadow: 'none',
|
||||
outline: 'none',
|
||||
border: 'none'
|
||||
}}
|
||||
bgColor={index % 2 == 0 ? 'white' : '#FBFCF3'} height='auto'
|
||||
justifyContent='center'
|
||||
w='full'
|
||||
alignItems={'center'}>
|
||||
<Flex w={{ base: '95%', sm: '95%', md: '80%', lg: '70%', xl: '50%' }}
|
||||
direction={{ base: 'column', sm: "column", md: 'row' }}
|
||||
marginY={4}
|
||||
>
|
||||
<Flex w={{ base: '15%', sm: '15%', md: '10%', lg: '10%', xl: '10%' }}
|
||||
justifyContent={{ base: 'flex-start', sm: 'flex-start', md: 'flex-end' }}
|
||||
alignItems='center'>
|
||||
<Image src={item.image}
|
||||
mt={2.5}
|
||||
mr={{ base: 0, sm: 0, md: 1.5 }}
|
||||
w={{ base: '70px', sm: '70px', md: '45px', lg: '45px', xl: '45px' }}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex w='90%'
|
||||
alignItems={'center'}>
|
||||
<Text
|
||||
color={colors.textColor}
|
||||
fontSize='2xl'
|
||||
className='font-noto-sans font-black'
|
||||
>{item.question}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Accordion.ItemTrigger>
|
||||
<Accordion.ItemContent>
|
||||
<Accordion.ItemBody
|
||||
justifyItems={'center'}>
|
||||
<Flex w={{ base: '95%', sm: '95%', md: '80%', lg: '70%', xl: '50%' }}>
|
||||
<Flex w={{ base: '3%', sm: '2%', md: '10%', lg: '10%', xl: '10%' }}
|
||||
justifyContent={{ base: 'flex-start', sm: 'flex-start', md: 'flex-end' }}
|
||||
alignItems='center'>
|
||||
</Flex>
|
||||
<Flex w={{ base: '97%', sm: '98%', md: '90%', lg: '90%', xl: '90%' }}
|
||||
direction={'column'}>
|
||||
{formatText(item.answer)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Accordion.ItemBody>
|
||||
</Accordion.ItemContent>
|
||||
</Accordion.Item>
|
||||
</MotionBox>
|
||||
);
|
||||
})}
|
||||
</Accordion.Root>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
export default Qa;
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
Avatar as ChakraAvatar,
|
||||
AvatarGroup as ChakraAvatarGroup,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
|
||||
|
||||
export interface AvatarProps extends ChakraAvatar.RootProps {
|
||||
name?: string
|
||||
src?: string
|
||||
srcSet?: string
|
||||
loading?: ImageProps["loading"]
|
||||
icon?: React.ReactElement
|
||||
fallback?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||
function Avatar(props, ref) {
|
||||
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||
props
|
||||
return (
|
||||
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||
<ChakraAvatar.Fallback name={name}>
|
||||
{icon || fallback}
|
||||
</ChakraAvatar.Fallback>
|
||||
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||
{children}
|
||||
</ChakraAvatar.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const AvatarGroup = ChakraAvatarGroup
|
|
@ -0,0 +1,25 @@
|
|||
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||
icon?: React.ReactNode
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
rootRef?: React.Ref<HTMLLabelElement>
|
||||
}
|
||||
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
function Checkbox(props, ref) {
|
||||
const { icon, children, inputProps, rootRef, ...rest } = props
|
||||
return (
|
||||
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraCheckbox.Control>
|
||||
{icon || <ChakraCheckbox.Indicator />}
|
||||
</ChakraCheckbox.Control>
|
||||
{children != null && (
|
||||
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||
)}
|
||||
</ChakraCheckbox.Root>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,17 @@
|
|||
import type { ButtonProps } from "@chakra-ui/react"
|
||||
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuX } from "react-icons/lu"
|
||||
|
||||
export type CloseButtonProps = ButtonProps
|
||||
|
||||
export const CloseButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
CloseButtonProps
|
||||
>(function CloseButton(props, ref) {
|
||||
return (
|
||||
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||
{props.children ?? <LuX />}
|
||||
</ChakraIconButton>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,107 @@
|
|||
"use client"
|
||||
|
||||
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
|
||||
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
|
||||
import { ThemeProvider, useTheme } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { LuMoon, LuSun } from "react-icons/lu"
|
||||
|
||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||
|
||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="light" disableTransitionOnChange {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export type ColorMode = "light" | "dark"
|
||||
|
||||
export interface UseColorModeReturn {
|
||||
colorMode: ColorMode
|
||||
setColorMode: (colorMode: ColorMode) => void
|
||||
toggleColorMode: () => void
|
||||
}
|
||||
|
||||
export function useColorMode(): UseColorModeReturn {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
return {
|
||||
colorMode: resolvedTheme as ColorMode,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
}
|
||||
|
||||
export function useColorModeValue<T>(light: T, dark: T) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? dark : light
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
||||
}
|
||||
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||
|
||||
export const ColorModeButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ColorModeButtonProps
|
||||
>(function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
aria-label="Toggle color mode"
|
||||
size="sm"
|
||||
ref={ref}
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: "5",
|
||||
height: "5",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
})
|
||||
|
||||
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||
function LightMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color="fg"
|
||||
display="contents"
|
||||
className="chakra-theme light"
|
||||
colorPalette="gray"
|
||||
colorScheme="light"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||
function DarkMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color="fg"
|
||||
display="contents"
|
||||
className="chakra-theme dark"
|
||||
colorPalette="gray"
|
||||
colorScheme="dark"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,62 @@
|
|||
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface DialogContentProps extends ChakraDialog.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
backdrop?: boolean
|
||||
}
|
||||
|
||||
export const DialogContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DialogContentProps
|
||||
>(function DialogContent(props, ref) {
|
||||
const {
|
||||
children,
|
||||
portalled = true,
|
||||
portalRef,
|
||||
backdrop = true,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
{backdrop && <ChakraDialog.Backdrop />}
|
||||
<ChakraDialog.Positioner>
|
||||
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDialog.Content>
|
||||
</ChakraDialog.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const DialogCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraDialog.CloseTriggerProps
|
||||
>(function DialogCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraDialog.CloseTrigger
|
||||
position="absolute"
|
||||
top="2"
|
||||
insetEnd="2"
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<CloseButton size="sm" ref={ref}>
|
||||
{props.children}
|
||||
</CloseButton>
|
||||
</ChakraDialog.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const DialogRoot = ChakraDialog.Root
|
||||
export const DialogFooter = ChakraDialog.Footer
|
||||
export const DialogHeader = ChakraDialog.Header
|
||||
export const DialogBody = ChakraDialog.Body
|
||||
export const DialogBackdrop = ChakraDialog.Backdrop
|
||||
export const DialogTitle = ChakraDialog.Title
|
||||
export const DialogDescription = ChakraDialog.Description
|
||||
export const DialogTrigger = ChakraDialog.Trigger
|
||||
export const DialogActionTrigger = ChakraDialog.ActionTrigger
|
|
@ -0,0 +1,52 @@
|
|||
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
offset?: ChakraDrawer.ContentProps["padding"]
|
||||
}
|
||||
|
||||
export const DrawerContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DrawerContentProps
|
||||
>(function DrawerContent(props, ref) {
|
||||
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraDrawer.Positioner padding={offset}>
|
||||
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDrawer.Content>
|
||||
</ChakraDrawer.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const DrawerCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraDrawer.CloseTriggerProps
|
||||
>(function DrawerCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraDrawer.CloseTrigger
|
||||
position="absolute"
|
||||
top="2"
|
||||
insetEnd="2"
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<CloseButton size="sm" ref={ref} />
|
||||
</ChakraDrawer.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const DrawerTrigger = ChakraDrawer.Trigger
|
||||
export const DrawerRoot = ChakraDrawer.Root
|
||||
export const DrawerFooter = ChakraDrawer.Footer
|
||||
export const DrawerHeader = ChakraDrawer.Header
|
||||
export const DrawerBody = ChakraDrawer.Body
|
||||
export const DrawerBackdrop = ChakraDrawer.Backdrop
|
||||
export const DrawerDescription = ChakraDrawer.Description
|
||||
export const DrawerTitle = ChakraDrawer.Title
|
||||
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
|
|
@ -0,0 +1,33 @@
|
|||
import { Field as ChakraField } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
errorText?: React.ReactNode
|
||||
optionalText?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||
function Field(props, ref) {
|
||||
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||
props
|
||||
return (
|
||||
<ChakraField.Root ref={ref} {...rest}>
|
||||
{label && (
|
||||
<ChakraField.Label>
|
||||
{label}
|
||||
<ChakraField.RequiredIndicator fallback={optionalText} />
|
||||
</ChakraField.Label>
|
||||
)}
|
||||
{children}
|
||||
{helperText && (
|
||||
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
|
||||
)}
|
||||
{errorText && (
|
||||
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||
)}
|
||||
</ChakraField.Root>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,53 @@
|
|||
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
|
||||
import { Group, InputElement } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface InputGroupProps extends BoxProps {
|
||||
startElementProps?: InputElementProps
|
||||
endElementProps?: InputElementProps
|
||||
startElement?: React.ReactNode
|
||||
endElement?: React.ReactNode
|
||||
children: React.ReactElement<InputElementProps>
|
||||
startOffset?: InputElementProps["paddingStart"]
|
||||
endOffset?: InputElementProps["paddingEnd"]
|
||||
}
|
||||
|
||||
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||
function InputGroup(props, ref) {
|
||||
const {
|
||||
startElement,
|
||||
startElementProps,
|
||||
endElement,
|
||||
endElementProps,
|
||||
children,
|
||||
startOffset = "6px",
|
||||
endOffset = "6px",
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const child =
|
||||
React.Children.only<React.ReactElement<InputElementProps>>(children)
|
||||
|
||||
return (
|
||||
<Group ref={ref} {...rest}>
|
||||
{startElement && (
|
||||
<InputElement pointerEvents="none" {...startElementProps}>
|
||||
{startElement}
|
||||
</InputElement>
|
||||
)}
|
||||
{React.cloneElement(child, {
|
||||
...(startElement && {
|
||||
ps: `calc(var(--input-height) - ${startOffset})`,
|
||||
}),
|
||||
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
|
||||
...children.props,
|
||||
})}
|
||||
{endElement && (
|
||||
<InputElement placement="end" {...endElementProps}>
|
||||
{endElement}
|
||||
</InputElement>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,59 @@
|
|||
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface PopoverContentProps extends ChakraPopover.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const PopoverContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PopoverContentProps
|
||||
>(function PopoverContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraPopover.Positioner>
|
||||
<ChakraPopover.Content ref={ref} {...rest} />
|
||||
</ChakraPopover.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraPopover.ArrowProps
|
||||
>(function PopoverArrow(props, ref) {
|
||||
return (
|
||||
<ChakraPopover.Arrow {...props} ref={ref}>
|
||||
<ChakraPopover.ArrowTip />
|
||||
</ChakraPopover.Arrow>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraPopover.CloseTriggerProps
|
||||
>(function PopoverCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraPopover.CloseTrigger
|
||||
position="absolute"
|
||||
top="1"
|
||||
insetEnd="1"
|
||||
{...props}
|
||||
asChild
|
||||
ref={ref}
|
||||
>
|
||||
<CloseButton size="sm" />
|
||||
</ChakraPopover.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverTitle = ChakraPopover.Title
|
||||
export const PopoverDescription = ChakraPopover.Description
|
||||
export const PopoverFooter = ChakraPopover.Footer
|
||||
export const PopoverHeader = ChakraPopover.Header
|
||||
export const PopoverRoot = ChakraPopover.Root
|
||||
export const PopoverBody = ChakraPopover.Body
|
||||
export const PopoverTrigger = ChakraPopover.Trigger
|
|
@ -0,0 +1,15 @@
|
|||
"use client"
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode"
|
||||
|
||||
export function Provider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface RadioProps extends ChakraRadioGroup.ItemProps {
|
||||
rootRef?: React.Ref<HTMLDivElement>
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||
function Radio(props, ref) {
|
||||
const { children, inputProps, rootRef, ...rest } = props
|
||||
return (
|
||||
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
|
||||
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraRadioGroup.ItemIndicator />
|
||||
{children && (
|
||||
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
|
||||
)}
|
||||
</ChakraRadioGroup.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const RadioGroup = ChakraRadioGroup.Root
|
|
@ -0,0 +1,82 @@
|
|||
import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface SliderProps extends ChakraSlider.RootProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
label?: React.ReactNode
|
||||
showValue?: boolean
|
||||
}
|
||||
|
||||
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||
function Slider(props, ref) {
|
||||
const { marks: marksProp, label, showValue, ...rest } = props
|
||||
const value = props.defaultValue ?? props.value
|
||||
|
||||
const marks = marksProp?.map((mark) => {
|
||||
if (typeof mark === "number") return { value: mark, label: undefined }
|
||||
return mark
|
||||
})
|
||||
|
||||
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
||||
|
||||
return (
|
||||
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
||||
{label && !showValue && (
|
||||
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
||||
)}
|
||||
{label && showValue && (
|
||||
<HStack justify="space-between">
|
||||
<ChakraSlider.Label>{label}</ChakraSlider.Label>
|
||||
<ChakraSlider.ValueText />
|
||||
</HStack>
|
||||
)}
|
||||
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
|
||||
<ChakraSlider.Track>
|
||||
<ChakraSlider.Range />
|
||||
</ChakraSlider.Track>
|
||||
<SliderThumbs value={value} />
|
||||
<SliderMarks marks={marks} />
|
||||
</ChakraSlider.Control>
|
||||
</ChakraSlider.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function SliderThumbs(props: { value?: number[] }) {
|
||||
const { value } = props
|
||||
return (
|
||||
<For each={value}>
|
||||
{(_, index) => (
|
||||
<ChakraSlider.Thumb key={index} index={index}>
|
||||
<ChakraSlider.HiddenInput />
|
||||
</ChakraSlider.Thumb>
|
||||
)}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
|
||||
interface SliderMarksProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
}
|
||||
|
||||
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
|
||||
function SliderMarks(props, ref) {
|
||||
const { marks } = props
|
||||
if (!marks?.length) return null
|
||||
|
||||
return (
|
||||
<ChakraSlider.MarkerGroup ref={ref}>
|
||||
{marks.map((mark, index) => {
|
||||
const value = typeof mark === "number" ? mark : mark.value
|
||||
const label = typeof mark === "number" ? undefined : mark.label
|
||||
return (
|
||||
<ChakraSlider.Marker key={index} value={value}>
|
||||
<ChakraSlider.MarkerIndicator />
|
||||
{label}
|
||||
</ChakraSlider.Marker>
|
||||
)
|
||||
})}
|
||||
</ChakraSlider.MarkerGroup>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,46 @@
|
|||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
content: React.ReactNode
|
||||
contentProps?: ChakraTooltip.ContentProps
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
function Tooltip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
disabled,
|
||||
portalled = true,
|
||||
content,
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (disabled) return children
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraTooltip.Positioner>
|
||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||
{showArrow && (
|
||||
<ChakraTooltip.Arrow>
|
||||
<ChakraTooltip.ArrowTip />
|
||||
</ChakraTooltip.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraTooltip.Content>
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
},
|
||||
)
|
|
@ -0,0 +1,49 @@
|
|||
/* Font Family Utility Classes */
|
||||
.font-melle {
|
||||
font-family: 'MElleHK', sans-serif;
|
||||
}
|
||||
|
||||
.font-noto-sans {
|
||||
font-family: 'NotoSansCJKtc', sans-serif;
|
||||
}
|
||||
|
||||
.font-noto-mono {
|
||||
font-family: 'NotoSansMonoCJKtc', monospace;
|
||||
}
|
||||
|
||||
.font-noto-serif {
|
||||
font-family: 'NotoSerifCJKjp', serif;
|
||||
}
|
||||
|
||||
/* Font Weight Utility Classes */
|
||||
.font-thin {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.font-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.font-demi-light {
|
||||
font-weight: 350;
|
||||
}
|
||||
|
||||
.font-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-xbold {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.font-black {
|
||||
font-weight: 900;
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/* MElleHK Font Family */
|
||||
@font-face {
|
||||
font-family: 'MElleHK';
|
||||
src: url('../assets/fonts/MElleHK-Light.OTF') format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MElleHK';
|
||||
src: url('../assets/fonts/MElleHK-Medium.OTF') format('opentype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MElleHK';
|
||||
src: url('../assets/fonts/MElleHK-Xbold.otf') format('opentype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* NotoSansCJKtc Font Family */
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-Thin.otf') format('opentype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-Light.otf') format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-DemiLight.otf') format('opentype');
|
||||
font-weight: 350;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-Regular.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-Medium.otf') format('opentype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-Bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansCJKtc';
|
||||
src: url('../assets/fonts/NotoSansCJKtc-Black.otf') format('opentype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* NotoSansMonoCJKtc Font Family */
|
||||
@font-face {
|
||||
font-family: 'NotoSansMonoCJKtc';
|
||||
src: url('../assets/fonts/NotoSansMonoCJKtc-Regular.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'NotoSansMonoCJKtc';
|
||||
src: url('../assets/fonts/NotoSansMonoCJKtc-Bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* NotoSerifCJKjp Font Family */
|
||||
@font-face {
|
||||
font-family: 'NotoSerifCJKjp';
|
||||
src: url('../assets/fonts/NotoSerifCJKjp-Black.otf') format('opentype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'NotoSansCJKtc', 'MElleHK', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: block;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Provider } from "@/components/ui/provider"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
import "./fonts.css"
|
||||
import "./colors.css"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Provider>
|
||||
<App />
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(),tsconfigPaths()],
|
||||
})
|