This commit is contained in:
2025-03-16 00:10:11 +08:00
parent e7cf1c28b0
commit a14b206d25
84 changed files with 8752 additions and 0 deletions

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

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" />