This commit is contained in:
philipcheung 2025-03-16 00:10:11 +08:00
parent e7cf1c28b0
commit a14b206d25
84 changed files with 8752 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?

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
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

28
eslint.config.js Normal file
View File

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

BIN
images/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

BIN
images/best5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

BIN
images/bgyellow.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

BIN
images/bigheart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

BIN
images/conpareheart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

BIN
images/cook_mb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

BIN
images/cook_pc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
images/headerlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
images/hero1oil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
images/hero1text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
images/mboilinfogroup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
images/mboilinfotitle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
images/oil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
images/oilchart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
images/oilinfobg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/oilinfofooterbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
images/oilinfooil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
images/oilinfotitle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
images/oilmobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
images/oldman.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

BIN
images/pcoilinfogroup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
images/people2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
images/q1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
images/q2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
images/q3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
images/q4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
images/q5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
images/qtext.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
images/text1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
images/title1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
images/woodtable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

13
index.html Normal file
View File

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

6581
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

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

1
public/vite.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="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

42
src/App.css Normal file
View File

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

60
src/App.tsx Normal file
View File

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

78
src/colors.css Normal file
View File

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

5
src/colors.tsx Normal file
View File

@ -0,0 +1,5 @@
export const colors = {
textColor: '#075C39',
backgroundColor: '#FFFBCE',
topBarColor: '#92C000',
}

143
src/components/bestoil.tsx Normal file
View File

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

View File

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

17
src/components/header.tsx Normal file
View File

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

116
src/components/hero1.tsx Normal file
View File

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

197
src/components/hero2.tsx Normal file
View File

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

236
src/components/oil_info.tsx Normal file
View File

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

152
src/components/qa.tsx Normal file
View File

@ -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-36 - Omega-6
\t SFA - `
},
{
image: '/images/q5.png',
question: "食油種類太多,點揀先唔會中伏?",
answer: `市面上的食油種類繁多,從花生油、粟米油、芥花籽油、橄欖油、米糠油到牛油果油,每種油都有不同的營養價值和適用範圍。要選擇真正健康和適合日常使用的食油,只需記住 3 個關鍵原則:
200°C的食油
MUFAPUFA
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

49
src/fonts.css Normal file
View File

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

181
src/index.css Normal file
View File

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

15
src/main.tsx Normal file
View File

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

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

31
tsconfig.app.json Normal file
View File

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

7
tsconfig.json Normal file
View File

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

24
tsconfig.node.json Normal file
View File

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

7
vite.config.ts Normal file
View File

@ -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()],
})