Compare commits
53 Commits
98730da5f5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e6a2bdad0b | |||
| 1a77719dfa | |||
| aa4dde3d19 | |||
| 9f68f445b9 | |||
| e8e820f710 | |||
| 6b3d7ae3f6 | |||
| 3d3974ef6d | |||
| 18735a884d | |||
| 6cb762bf76 | |||
| 3530f6d52e | |||
| 671b0de875 | |||
| 2113724aaa | |||
| ec349edd42 | |||
| 7534df0f83 | |||
| e7eccb30e0 | |||
| d8448f70d4 | |||
| a247df6d11 | |||
| 82d434017d | |||
| b09707eb95 | |||
| 8dd83eed4f | |||
| 8b60b0c9b6 | |||
| fcacafa237 | |||
| 96066a64d5 | |||
| f5392cf597 | |||
| 21cc9905b7 | |||
| 6c7384e8b4 | |||
| 8e7e69e7ce | |||
| 56927ac9f3 | |||
| 708ce80dbb | |||
| f16f0fdb32 | |||
| c63f36fb59 | |||
| 11c699a5a6 | |||
| a8a918dabc | |||
| 46f5f424bc | |||
| c9931d670e | |||
| 144a362a16 | |||
| 2f4e90f0c4 | |||
| 999ee7224f | |||
| 736578dae3 | |||
| 3c39db54d2 | |||
| 1403217253 | |||
| 74027dc6db | |||
| 0ef384d5d2 | |||
| eb6ed870a7 | |||
| b68899b91a | |||
| 0718271246 | |||
| b190bf3357 | |||
| 5d7814f983 | |||
| 6392ee020f | |||
| 63ba11891f | |||
| c4181b48b1 | |||
| 3cdcb28829 | |||
| 9d7a9b8d94 |
0
.github/instructions/copilot_guide.instructions.md
vendored
Normal file
26
Dockerfile
@@ -1,26 +1,22 @@
|
||||
# Use an official Node.js runtime as a parent image
|
||||
FROM node:18-alpine
|
||||
FROM node:20 AS build-stage
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
COPY package*.json /app/
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
COPY ./ /app/
|
||||
|
||||
ARG VITE_API_URL=${VITE_API_URL}
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Install a lightweight web server
|
||||
RUN npm install -g serve
|
||||
|
||||
# Set the command to run the web server
|
||||
CMD ["serve", "-s", "dist"]
|
||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
||||
FROM nginx:1
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 3000
|
||||
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
|
||||
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
|
||||
|
||||
@@ -6,13 +6,26 @@ services:
|
||||
container_name: healthy-oil
|
||||
|
||||
ports:
|
||||
- "3010:3000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- "3015:80"
|
||||
|
||||
networks:
|
||||
- gitea_network
|
||||
- traefik-public
|
||||
- default
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-public
|
||||
|
||||
- traefik.http.services.healthy-oil-frontend.loadbalancer.server.port=80
|
||||
|
||||
- traefik.http.routers.healthy-oil-frontend-http.rule=Host(`healthy-oil.develop-cat.com`)
|
||||
- traefik.http.routers.healthy-oil-frontend-http.entrypoints=http
|
||||
- traefik.http.routers.healthy-oil-frontend-http.service=healthy-oil-frontend
|
||||
|
||||
- traefik.http.routers.healthy-oil-frontend-https.rule=Host(`healthy-oil.develop-cat.com`)
|
||||
- traefik.http.routers.healthy-oil-frontend-https.entrypoints=https
|
||||
- traefik.http.routers.healthy-oil-frontend-https.tls=true
|
||||
- traefik.http.routers.healthy-oil-frontend-https.service=healthy-oil-frontend
|
||||
|
||||
networks:
|
||||
gitea_network:
|
||||
traefik-public:
|
||||
external: true
|
||||
|
||||
11
nginx copy.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
include /etc/nginx/extra-conf.d/*.conf;
|
||||
}
|
||||
9
nginx-backend-not-found.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
location /api {
|
||||
return 404;
|
||||
}
|
||||
location /docs {
|
||||
return 404;
|
||||
}
|
||||
location /redoc {
|
||||
return 404;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
server {
|
||||
listen 3010;
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
129
package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"react-facebook": "^9.0.12",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-lazy-load-image-component": "^1.6.3",
|
||||
"react-social-media-embed": "^2.5.18",
|
||||
"react-social-plugins": "^2.1.0",
|
||||
"snippet": "^0.1.0"
|
||||
},
|
||||
@@ -1911,6 +1912,12 @@
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/youtube-player": {
|
||||
"version": "5.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.11.tgz",
|
||||
"integrity": "sha512-pM41CDBqJqBmTeJWnF7NOGz82IQoYOhqzMYXv5vKCXBqGiYSLldxMtpCk6KAEtADTy49S45AriYaCaZyeUX38Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz",
|
||||
@@ -3683,6 +3690,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-table": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz",
|
||||
@@ -4201,7 +4214,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
@@ -4975,6 +4987,12 @@
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/load-script": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -5695,6 +5713,16 @@
|
||||
"react": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-html-props": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-html-props/-/react-html-props-2.1.1.tgz",
|
||||
"integrity": "sha512-tM+YCYlr90m3JontKUAa+gNVU2zkyprlCS7OQ9aa3z2MfyJjAioJzrSmi1Vef/+UCTE6CQlPqLX4ebdLIJDKxw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
@@ -5733,6 +5761,40 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-social-media-embed": {
|
||||
"version": "2.5.18",
|
||||
"resolved": "https://registry.npmjs.org/react-social-media-embed/-/react-social-media-embed-2.5.18.tgz",
|
||||
"integrity": "sha512-+PkzLRGAwnySkxKajaiK5VD+EjOhlFsh/vjNxgHsDfKBTseDpFxPrMXXQWkk6BRCwFBNVWX+V1HZ9AU0y54Wgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/youtube-player": "^5.5.5",
|
||||
"classnames": "^2.5.1",
|
||||
"react-html-props": "^2.0.3",
|
||||
"react-sub-unsub": "^2.2.1",
|
||||
"react-twitter-embed": "^4.0.4",
|
||||
"react-youtube": "^10.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-social-media-embed/node_modules/react-twitter-embed": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-twitter-embed/-/react-twitter-embed-4.0.4.tgz",
|
||||
"integrity": "sha512-2JIL7qF+U62zRzpsh6SZDXNI3hRNVYf5vOZ1WRcMvwKouw+xC00PuFaD0aEp2wlyGaZ+f4x2VvX+uDadFQ3HVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scriptjs": "^2.5.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-social-plugins": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-social-plugins/-/react-social-plugins-2.1.0.tgz",
|
||||
@@ -5757,6 +5819,33 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-sub-unsub": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-sub-unsub/-/react-sub-unsub-2.2.8.tgz",
|
||||
"integrity": "sha512-o3tmiOOZPdQUCmRhkdCHXRFLOHnCwdz/N3QZ1JQ14fQGA2HysKMF0kWu56ERnQUCK7wYVCQzI8pFbnivAYNQ+A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-youtube": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz",
|
||||
"integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"prop-types": "15.8.1",
|
||||
"youtube-player": "5.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -5895,6 +5984,12 @@
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scriptjs": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
||||
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
|
||||
@@ -6000,6 +6095,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sister": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
|
||||
"integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -6804,6 +6905,32 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/youtube-player": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
||||
"integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^2.6.6",
|
||||
"load-script": "^1.0.0",
|
||||
"sister": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/youtube-player/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/youtube-player/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"react-facebook": "^9.0.12",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-lazy-load-image-component": "^1.6.3",
|
||||
"react-social-media-embed": "^2.5.18",
|
||||
"react-social-plugins": "^2.1.0",
|
||||
"snippet": "^0.1.0"
|
||||
},
|
||||
|
||||
16
public/.htaccess
Normal file
@@ -0,0 +1,16 @@
|
||||
Options -MultiViews
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Don't rewrite files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Don't rewrite assets, images, fonts
|
||||
RewriteCond %{REQUEST_URI} ^/(assets|images|fonts)/ [OR]
|
||||
RewriteCond %{REQUEST_URI} \.(js|css|jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|otf|eot)$ [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Route everything else to index.html
|
||||
RewriteRule ^ /index.html [L]
|
||||
BIN
public/images/Lion_head.webp
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
public/images/fb.webp
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
public/images/new/ans_step1.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/images/new/ans_step2.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/new/ans_step3.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/new/ans_step4.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/images/new/blood.webp
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
public/images/new/brain.webp
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
public/images/new/buttons.webp
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
public/images/new/heart.webp
Normal file
|
After Width: | Height: | Size: 345 KiB |
BIN
public/images/new/liver.webp
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
public/images/new/new_hero_mobile_bg.webp
Normal file
|
After Width: | Height: | Size: 630 KiB |
BIN
public/images/new/new_hero_pc_bg.webp
Normal file
|
After Width: | Height: | Size: 853 KiB |
|
Before Width: | Height: | Size: 135 KiB |
BIN
public/images/new/oil_new.webp
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
public/images/new/recipe_button.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/images/new/recipe_title.webp
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
public/images/new/skin.webp
Normal file
|
After Width: | Height: | Size: 218 KiB |
@@ -1,11 +1,15 @@
|
||||
import { Stack, Flex, Image, Text, Box } from '@chakra-ui/react';
|
||||
import { colors } from '../colors';
|
||||
|
||||
const footerText = `*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||
+根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算
|
||||
#每食用分量含0克反式脂肪
|
||||
▲Omega-6不飽和脂肪酸在體內代謝會產生花生四烯酸,進而產生促發炎的前列腺素,導致血管收縮及慢性發炎
|
||||
▼營萃護心油的omega-3比例較一般花生油、粟米油高,特別有助平衡外出用餐多的都市人其身體脂肪酸比例,增加抗發炎,維持心血管健康`
|
||||
// const footerText = `*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||
// +根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算
|
||||
// #每食用分量含0克反式脂肪
|
||||
// ▲Omega-6不飽和脂肪酸在體內代謝會產生花生四烯酸,進而產生促發炎的前列腺素,導致血管收縮及慢性發炎
|
||||
// ▼營萃護心油的omega-3比例較一般花生油、粟米油高,特別有助平衡外出用餐多的都市人其身體脂肪酸比例,增加抗發炎,維持心血管健康`
|
||||
const footerText = `¹World Health Organization (2019). REPLACE trans fat: an action package to eliminate industrially produced transfatty acids. Geneva: World Health Organization. ISBN: 978-92-4-151499-6.
|
||||
² 根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算
|
||||
|
||||
`
|
||||
function Footer() {
|
||||
|
||||
const formatText = (text: string) => {
|
||||
@@ -51,7 +55,8 @@ function Footer() {
|
||||
<Flex
|
||||
alignItems={'flex-end'}
|
||||
w={{ base: '100%', sm: '100%', md: '60%', lg: '55%', xl: '55%' }}
|
||||
h={{ base: '180px', sm: '180px', md: '130px' }}
|
||||
h={{ base: '100px', sm: '80px', md: '130px' }}
|
||||
//h={{ base: '180px', sm: '180px', md: '130px' }}
|
||||
|
||||
>
|
||||
|
||||
@@ -64,7 +69,7 @@ function Footer() {
|
||||
className='font-noto-sans font-regular'
|
||||
color={colors.textColor}
|
||||
fontSize={'10px'}
|
||||
mt={2}
|
||||
// mt={2}
|
||||
ml={2}
|
||||
>
|
||||
{`Copyright © ${currentYear}合興食油(香港)有限公司 版權所有,不得轉載。`}
|
||||
|
||||
@@ -1,13 +1,262 @@
|
||||
import { Box, Flex, Image, } from '@chakra-ui/react';
|
||||
import {colors} from '../colors';
|
||||
import { Box, Flex, Image, VStack, Text, Link } from '@chakra-ui/react';
|
||||
import { GiHamburgerMenu } from "react-icons/gi";
|
||||
import { IoCloseSharp } from "react-icons/io5";
|
||||
import { colors } from '../colors';
|
||||
import { useState, useRef, useLayoutEffect, useEffect } from 'react';
|
||||
import {
|
||||
DrawerBody,
|
||||
DrawerContent,
|
||||
DrawerRoot,
|
||||
} from '@/components/ui/drawer';
|
||||
import { useRouter, useRouterState } from '@tanstack/react-router';
|
||||
|
||||
interface HeaderProps {
|
||||
showMenu?: boolean;
|
||||
}
|
||||
|
||||
function Header({ showMenu = false }: HeaderProps) {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const lastScrollY = useRef(0);
|
||||
const router = useRouter();
|
||||
const { location } = useRouterState();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateHeight = () => {
|
||||
if (headerRef.current) {
|
||||
setHeaderHeight(headerRef.current.getBoundingClientRect().height);
|
||||
}
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', updateHeight);
|
||||
}
|
||||
|
||||
const observer = typeof ResizeObserver !== 'undefined' && headerRef.current
|
||||
? new ResizeObserver(updateHeight)
|
||||
: undefined;
|
||||
|
||||
if (observer && headerRef.current) {
|
||||
observer.observe(headerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', updateHeight);
|
||||
}
|
||||
observer?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (currentScrollY > lastScrollY.current && currentScrollY > 100) {
|
||||
setIsVisible(false);
|
||||
} else {
|
||||
setIsVisible(true);
|
||||
}
|
||||
|
||||
lastScrollY.current = currentScrollY;
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const targetId = sectionId.replace('#', '');
|
||||
const isOn40PlusPage = location.pathname === '/40plus';
|
||||
const element = isOn40PlusPage ? document.getElementById(targetId) : null;
|
||||
|
||||
if (element) {
|
||||
// Close drawer first, then scroll after animation completes
|
||||
if (isDrawerOpen) {
|
||||
setIsDrawerOpen(false);
|
||||
// Wait for drawer animation to complete (300ms) before scrolling
|
||||
setTimeout(() => {
|
||||
performScroll(element);
|
||||
}, 350);
|
||||
} else {
|
||||
performScroll(element);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.sessionStorage.setItem('pendingScrollSection', targetId);
|
||||
window.dispatchEvent(new CustomEvent('pending-scroll-section'));
|
||||
}
|
||||
|
||||
if (!isOn40PlusPage) {
|
||||
router.navigate({ to: '/40plus' });
|
||||
}
|
||||
};
|
||||
|
||||
const performScroll = (element: HTMLElement) => {
|
||||
// Safari and mobile-compatible scrolling with multiple fallbacks
|
||||
const headerOffset = headerHeight || 0;
|
||||
const elementPosition = element.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + (window.pageYOffset || window.scrollY || document.documentElement.scrollTop) - headerOffset;
|
||||
|
||||
// Try smooth scrolling first
|
||||
try {
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} catch (e) {
|
||||
// Fallback 1: Try scrollTo without behavior
|
||||
try {
|
||||
window.scrollTo(0, offsetPosition);
|
||||
} catch (e2) {
|
||||
// Fallback 2: Use element.scrollIntoView
|
||||
const yOffset = -headerOffset;
|
||||
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuClick = (itemId: string) => {
|
||||
if (itemId === 'recipes') {
|
||||
const isOnRecipePage = location.pathname === '/40plus/recipe';
|
||||
|
||||
if (isOnRecipePage) {
|
||||
// Already on recipe page, just close drawer
|
||||
setIsDrawerOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to recipe page - force reload to ensure Instagram embeds work
|
||||
window.location.href = '/40plus/recipe';
|
||||
return;
|
||||
} else {
|
||||
// Scroll to section (drawer will be closed inside scrollToSection)
|
||||
scrollToSection(itemId);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ label: '40+常見健康問題', id: 'info' },
|
||||
{ label: '營萃護心油四大優勢', id: 'advantages' },
|
||||
{ label: '護心食譜', id: 'recipes' },
|
||||
{ label: '更多食用油健康真相', id: 'truth' },
|
||||
];
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<Box bg={colors.topBarColor} py={4} w="full">
|
||||
<Flex alignItems={'center'} justifyContent={'center'} direction="column">
|
||||
<>
|
||||
<Box
|
||||
ref={headerRef}
|
||||
bg={colors.topBarColor}
|
||||
py={4}
|
||||
w="full"
|
||||
position="fixed"
|
||||
top={0}
|
||||
zIndex={1000}
|
||||
transform={isVisible ? "translateY(0)" : "translateY(-100%)"}
|
||||
transition="transform 0.3s ease-in-out"
|
||||
data-header="true"
|
||||
>
|
||||
<Flex alignItems={'center'} justifyContent={'center'} direction="column" position="relative" w="full">
|
||||
<Image src="/images/header_logo.webp" alt="Logo" width="150px" />
|
||||
{showMenu && (
|
||||
<Box
|
||||
position="absolute"
|
||||
right={4}
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
cursor="pointer"
|
||||
onClick={() => setIsDrawerOpen((prev) => !prev)}
|
||||
_hover={{ opacity: 0.8 }}
|
||||
>
|
||||
{isDrawerOpen ? (
|
||||
<IoCloseSharp
|
||||
size={40}
|
||||
color="#2E683D"
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<GiHamburgerMenu
|
||||
size={40}
|
||||
color="#2E683D"
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<DrawerRoot open={isDrawerOpen} onOpenChange={(e) => setIsDrawerOpen(e.open)} placement="end">
|
||||
<DrawerContent
|
||||
bgColor={'#92C100'}
|
||||
width={{ base: "100%", sm: "100%", md: "400px" }}
|
||||
maxWidth={{ base: "100%", sm: "100%", md: "400px" }}
|
||||
margin={0}
|
||||
padding={0}
|
||||
positionerProps={{
|
||||
style: {
|
||||
top: headerHeight ? `${headerHeight}px` : undefined,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: headerHeight ? `calc(100vh - ${headerHeight}px)` : '100vh',
|
||||
},
|
||||
zIndex: 900,
|
||||
}}
|
||||
>
|
||||
<DrawerBody
|
||||
padding={0}
|
||||
margin={0}
|
||||
>
|
||||
<VStack align="stretch" gap={4} mt={4} justifyItems={'center'}>
|
||||
{menuItems.map((item, index) => (
|
||||
<Box
|
||||
h="70px"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
|
||||
key={index}
|
||||
onClick={() => handleMenuClick(item.id)}
|
||||
p={3}
|
||||
_hover={{ bg: '#458B02' }}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Text className='font-noto-sans font-xbold' fontSize="3xl" ml={10}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
<VStack
|
||||
w='100%'
|
||||
alignItems={'center'}
|
||||
>
|
||||
<Link href={"https://www.facebook.com/profile.php?id=100064320806613"}
|
||||
_focus={{ outline: 'none', boxShadow: 'none' }}
|
||||
_active={{ outline: 'none', boxShadow: 'none' }}>
|
||||
<Image
|
||||
src={'/images/fb.webp'}
|
||||
alt="salespoint"
|
||||
w={"350px"}
|
||||
loading='lazy'
|
||||
/>
|
||||
</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
|
||||
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</DrawerRoot>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
14
src/components/header2.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Box, Flex, Image, } from '@chakra-ui/react';
|
||||
import { colors } from '../colors';
|
||||
|
||||
function Header2() {
|
||||
return (
|
||||
<Box bg={colors.topBarColor} py={4} w="full">
|
||||
<Flex alignItems={'center'} justifyContent={'center'} direction="column">
|
||||
<Image src="/images/header_logo.webp" alt="Logo" width="150px" />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header2;
|
||||
@@ -1,20 +1,28 @@
|
||||
import { Outlet } from '@tanstack/react-router'
|
||||
import { Outlet, useRouterState } from '@tanstack/react-router'
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import Header from './header'
|
||||
|
||||
import Footer from './footer'
|
||||
import OldFooter from './oldFooter'
|
||||
|
||||
|
||||
|
||||
function Layout() {
|
||||
const { location } = useRouterState()
|
||||
|
||||
// Use oldFooter for main page (/), footer for /40+ pages
|
||||
const isMainPage = location.pathname === '/'
|
||||
|
||||
return (
|
||||
<Container maxW="100vw" p={0} m={0} mt={0}>
|
||||
<Header />
|
||||
{/* <Header showMenu={showMenu} /> */}
|
||||
<Box position="relative">
|
||||
<Outlet />
|
||||
</Box>
|
||||
<Footer />
|
||||
{isMainPage ? <OldFooter /> : <Footer />}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ interface CyclingImageProps {
|
||||
right?: any
|
||||
bottom?: any
|
||||
display?: any
|
||||
transform?: any
|
||||
cycleDuration?: number // Duration of one complete cycle in seconds
|
||||
intensity?: number // How dark/light it gets (0-1, where 1 is maximum)
|
||||
timingFunction?: string // CSS timing function
|
||||
@@ -31,6 +32,7 @@ const CyclingImage = ({
|
||||
right,
|
||||
bottom,
|
||||
display,
|
||||
transform,
|
||||
cycleDuration = 3,
|
||||
intensity = 0.5,
|
||||
timingFunction = 'ease-in-out',
|
||||
@@ -95,6 +97,7 @@ const CyclingImage = ({
|
||||
w={w}
|
||||
maxW={maxW}
|
||||
left={left}
|
||||
transform={transform}
|
||||
top={top}
|
||||
right={right}
|
||||
bottom={bottom}
|
||||
|
||||
@@ -1,21 +1,61 @@
|
||||
import { Box, Stack, Image, Flex, Text, SimpleGrid } from '@chakra-ui/react'
|
||||
import { Box, Stack, Image, Flex, Text, SimpleGrid, HStack } from '@chakra-ui/react'
|
||||
import { motion, useInView } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
const MotionImage = motion.create(Image)
|
||||
const MotionFlex = motion.create(Flex)
|
||||
const MotionBox = motion.create(Box)
|
||||
import { colors } from '../../colors';
|
||||
const MotionStack = motion(Stack);
|
||||
interface InfoData {
|
||||
id: number
|
||||
title: string
|
||||
image: string
|
||||
titleImage: string
|
||||
titleImage2: string
|
||||
description: string
|
||||
const MotionHStack = motion(HStack);
|
||||
const MotionSimpleGrid = motion.create(SimpleGrid);
|
||||
const MotionText = motion(Text);
|
||||
import { colors } from '../../colors';
|
||||
|
||||
function contextPeopleImageMt(index: number) {
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
return { md: -4, lg: "-3.5vw", xl: "-1.5vw" };
|
||||
|
||||
case 1:
|
||||
return { md: -1, lg: "-1vw", xl: "0vw" };
|
||||
|
||||
case 2:
|
||||
return { md: -4, lg: "-3.5vw", xl: "-1.5vw" };
|
||||
|
||||
case 3:
|
||||
return { md: -4, lg: "-3.5vw", xl: "-1.5vw" };
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function contextPeopleImageSize(index: number) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return { md: '20vw', lg: '15vw', xl: '12vw' };
|
||||
case 1:
|
||||
return { md: '20vw', lg: '20vw', xl: '16vw' };
|
||||
case 2:
|
||||
return { md: '20vw', lg: '10vw', xl: '8vw' };
|
||||
case 3:
|
||||
return { md: '20vw', lg: '25vw', xl: '16vw' };
|
||||
}
|
||||
}
|
||||
|
||||
function contextTitleImageSize(index: number) {
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
return { md: '20vw', lg: '20vw', xl: '12vw' };
|
||||
case 1:
|
||||
return { md: '20vw', lg: '20vw', xl: '13vw' };
|
||||
case 2:
|
||||
return { md: '20vw', lg: '25vw', xl: '16vw' };
|
||||
case 3:
|
||||
return { md: '20vw', lg: '25vw', xl: '16vw' };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const oilCube = [
|
||||
{
|
||||
@@ -49,19 +89,20 @@ const info = [
|
||||
{
|
||||
id: 1,
|
||||
title: "減少壞膽固醇",
|
||||
description: `**根據世界衛生組織數據,高反式脂肪攝取可令全因死亡率上升34%,**冠心病發病率上升 21%,及死亡率上升24%。全球每年因反式脂肪導致心血管疾病死亡病例已高達54萬人¹。
|
||||
description: `**根據世界衛生組織數據,高反式脂肪攝取可令全因死亡率上升34%**,冠心病發病率上升 21%,及死亡率上升24%。全球每年因反式脂肪導致心血管疾病死亡病例已高達54萬人¹。
|
||||
|
||||
**獅球嘜營萃護心油每食用份量含0克反式脂肪,**有助減少壞膽固醇,減低患心臟病嘅風險。`,
|
||||
**獅球嘜營萃護心油每食用份量含0克反式脂肪**,有助減少壞膽固醇,減低患心臟病嘅風險。`,
|
||||
image: "/images/new/1.webp",
|
||||
titleImage: "/images/new/adv_img1.webp",
|
||||
titleImage2: "/images/new/adv1.webp"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "黃金比例脂肪酸調配",
|
||||
description: `根據世衛建議,脂肪攝取應以**不飽和脂肪為主,**當中包括多元與單元不飽和脂肪酸,同時應減少飽和脂肪攝取,以維持整體健康比例。
|
||||
title: `黃金比例
|
||||
脂肪酸調配`,
|
||||
description: `根據世衛建議,脂肪攝取應以**不飽和脂肪為主**,當中包括單元與多元不飽和脂肪酸,同時應減少飽和脂肪攝取,以維持整體健康比例。
|
||||
|
||||
獅球嘜營萃護心油就係根據呢個原則調配出符合**世衛黃金比例**嘅配方²,幫助身體攝取每日必需嘅脂肪酸,並促進**脂溶性維他命A、D、E、K**嘅吸收,全面支援日常健康所需。 `,
|
||||
獅球嘜營萃護心油就係根據呢個原則調配出符合**世衛黃金比例**嘅配方²,幫助身體攝取每日必需脂肪酸,並促進**脂溶性維他命A、D、E、K**嘅吸收,全面支援日常健康所需。 `,
|
||||
image: "/images/new/2.webp",
|
||||
titleImage: "/images/new/adv_img2.webp",
|
||||
titleImage2: "/images/new/adv2.webp"
|
||||
@@ -69,9 +110,9 @@ const info = [
|
||||
{
|
||||
id: 3,
|
||||
title: "比一般食油",
|
||||
description: `**Omega-3有助降低三酸甘油脂水平,**並喺一定程度上減少血管發炎反應,從而支援血管彈性及正常血液循環,為心臟健康打好基礎。
|
||||
description: `**Omega-3有助降低三酸甘油脂水平**,並喺一定程度上減少血管發炎反應,從而支援血管彈性及正常血液循環,為心臟健康打好基礎。
|
||||
|
||||
**獅球嘜營萃護心油內嘅Omega-3比例較一般花生油及粟米油高,**特別有助經常外出用餐嘅都市人平衡脂肪酸攝取比例,從而支援心血管健康。 `,
|
||||
**獅球嘜營萃護心油內嘅Omega-3比例較一般花生油及粟米油高**,特別有助經常外出用餐嘅都市人平衡脂肪酸攝取比例,從而支援心血管健康。 `,
|
||||
image: "/images/new/3.webp",
|
||||
titleImage: "/images/new/adv_img3.webp",
|
||||
titleImage2: "/images/new/adv3.webp"
|
||||
@@ -124,84 +165,331 @@ function Advantages() {
|
||||
|
||||
const oilCubesRef = useRef(null);
|
||||
const isOilCubesInView = useInView(oilCubesRef, { once: true });
|
||||
const mainRef = useRef(null);
|
||||
const isMainInView = useInView(mainRef, { once: true });
|
||||
return (
|
||||
<Box
|
||||
<MotionBox
|
||||
id="advantages"
|
||||
ref={mainRef}
|
||||
position="relative"
|
||||
w="100%"
|
||||
h="auto"
|
||||
bgColor="#9AC035"
|
||||
zIndex={3}
|
||||
overflow="hidden"
|
||||
justifyItems={'center'}
|
||||
overflow="hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isMainInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, sm: 1, md: 2 }}
|
||||
p={4}
|
||||
w={{ base: '50vw', sm: '90vw', md: '90vw', lg: '80vw', xl: '70vw' }}
|
||||
h={'auto'}
|
||||
|
||||
>
|
||||
{info.map((item) => (
|
||||
<Stack
|
||||
w={'100%'}
|
||||
alignItems="center"
|
||||
key={item.id}>
|
||||
<Image
|
||||
w={{ base: '40vw', sm: '180px', md: '80px', lg: '110px', xl: '110px' }}
|
||||
src={item.image}
|
||||
/>
|
||||
position="relative"
|
||||
w="100%"
|
||||
justifyItems={'center'}
|
||||
alignItems={'center'}
|
||||
gap={0}
|
||||
>
|
||||
|
||||
<Image
|
||||
mt={{ base: '-30px', sm: '-70px', md: '-30px', lg: '-50px', xl: '-50px' }}
|
||||
h={{ base: '40vw', sm: '80px', md: '60px', lg: '80px', xl: '80px' }}
|
||||
src={item.titleImage2}
|
||||
src='/images/new/heart.webp'
|
||||
w={{ base: '75vw', sm: '75vw', md: '40vw', lg: '40vw', xl: '40vw' }}
|
||||
/>
|
||||
|
||||
<Text
|
||||
className='font-melle font-xbold'
|
||||
fontSize={{ base: '7.5vw', sm: '7.5vw', md: '4vw', lg: '4vw', xl: '4vw' }}
|
||||
color={'white'}
|
||||
mt={-2}
|
||||
>
|
||||
{'四大優勢,世一選擇!'}
|
||||
</Text>
|
||||
|
||||
</Stack>
|
||||
|
||||
|
||||
<MotionSimpleGrid
|
||||
display={{ base: 'none', sm: 'none', md: 'block', lg: 'block', xl: 'block' }}
|
||||
columns={{ base: 1, sm: 1, md: 1 }}
|
||||
p={4}
|
||||
pb={10}
|
||||
mt={20}
|
||||
w={{ base: '100%', sm: '95vw', md: '90vw', lg: '85vw', xl: '65vw' }}
|
||||
h={'auto'}
|
||||
mx="auto"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
|
||||
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
|
||||
|
||||
>
|
||||
{info.map((item, index) => (
|
||||
<MotionHStack
|
||||
w={'100%'}
|
||||
justifyContent={'flex-start'}
|
||||
alignItems={'flex-start'}
|
||||
key={item.id}
|
||||
mt={index != 0 ? 16 : 0}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.15, ease: "easeOut" }}
|
||||
>
|
||||
<MotionStack
|
||||
flex={{ md: 0.5, lg: 0.4, xl: 0.4 }}
|
||||
alignItems={'center'}>
|
||||
<MotionHStack
|
||||
alignItems={'flex-start'}
|
||||
mt={-4}
|
||||
>
|
||||
<MotionImage
|
||||
w={{ base: '45vw', sm: '180px', md: '80px', lg: '110px', xl: '5vw' }}
|
||||
src={item.image}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.1, ease: "easeOut" }}
|
||||
/>
|
||||
|
||||
<MotionStack
|
||||
mt={2}
|
||||
gap={0}
|
||||
justifyContent={'flex-start'}
|
||||
alignItems={'center'}
|
||||
|
||||
>
|
||||
<MotionImage
|
||||
fit={'contain'}
|
||||
w={contextTitleImageSize(index)}
|
||||
src={item.titleImage2}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.2, ease: "easeOut" }}
|
||||
/>
|
||||
|
||||
<MotionText
|
||||
|
||||
className='font-melle font-xbold'
|
||||
fontWeight={800}
|
||||
textAlign={'center'}
|
||||
mt={-1}
|
||||
whiteSpace="pre-wrap"
|
||||
color='white'
|
||||
lineHeight="1"
|
||||
fontSize={{ md: '2.5vw', lg: '2.5vw', xl: '1.5vw' }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{item.title}
|
||||
</MotionText>
|
||||
|
||||
</MotionStack>
|
||||
</MotionHStack>
|
||||
|
||||
<MotionImage
|
||||
w={contextPeopleImageSize(index)}
|
||||
fit={'contain'}
|
||||
mt={contextPeopleImageMt(index)}
|
||||
src={item.titleImage}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.4, ease: "easeOut" }}
|
||||
/>
|
||||
</MotionStack>
|
||||
|
||||
|
||||
{index === 3 ? (
|
||||
<Box
|
||||
flex={0.6}
|
||||
ml={14}
|
||||
w='100%'
|
||||
display="flex"
|
||||
flexDirection={{ base: "row", sm: "row", md: "column" }}
|
||||
alignItems="flex-start"
|
||||
mt={5}
|
||||
ref={oilCubesRef}
|
||||
>
|
||||
<Flex flexWrap="wrap" justifyContent="flex-start" gap="15px" >
|
||||
{oilCube.map((item, index) => (
|
||||
<Stack
|
||||
key={index}
|
||||
w={{ md: '22vw', lg: '22vw', xl: '16vw' }}
|
||||
h={{ md: '8vw', lg: '8vw', xl: '5vw' }}
|
||||
bgImage={item.bgColor}
|
||||
roundedTopLeft={'30px'}
|
||||
roundedBottomRight={'30px'}
|
||||
p={4}
|
||||
justify="center"
|
||||
>
|
||||
<Text
|
||||
color="white"
|
||||
className='font-melle font-black'
|
||||
fontSize={{ md: "1.5vw", lg: "1.5vw", xl: "1.2vw" }}
|
||||
mb={-1}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text
|
||||
color={colors.backgroundColor}
|
||||
className='font-melle font-medium'
|
||||
fontSize={{ md: "1vw", lg: "1.0vw", xl: "0.8vw" }}
|
||||
mt={-1}
|
||||
>
|
||||
{renderCubeDescription(item.text)}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<MotionText
|
||||
w={{ base: '90%', sm: '80%', md: '90%', lg: '80%', xl: '80%' }}
|
||||
className="font-noto-sans font-demi-light"
|
||||
fontSize={{ base: 'xl', sm: 'xl', md: 'xl', lg: '2xl', xl: '2xl' }}
|
||||
color="white"
|
||||
flex={{ md: 0.5, lg: 0.6, xl: 0.6 }}
|
||||
ml={14}
|
||||
alignSelf="flex-start"
|
||||
lineHeight="1.8"
|
||||
textAlign="left"
|
||||
whiteSpace="pre-wrap"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.5, ease: "easeOut" }}
|
||||
>
|
||||
{renderDescription(item.description)}
|
||||
</MotionText>)}
|
||||
|
||||
|
||||
|
||||
|
||||
</MotionHStack>
|
||||
))}
|
||||
</MotionSimpleGrid>
|
||||
|
||||
|
||||
|
||||
<MotionSimpleGrid
|
||||
display={{ base: 'block', sm: 'block', md: 'none', lg: 'none', xl: 'none' }}
|
||||
columns={{ base: 1, sm: 1, md: 1 }}
|
||||
p={4}
|
||||
pb={10}
|
||||
w={{ base: '100%', sm: '95vw', md: '90vw', lg: '80vw', xl: '70vw' }}
|
||||
h={'auto'}
|
||||
mx="auto"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
|
||||
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{info.map((item, index) => (
|
||||
<MotionStack
|
||||
w={'100%'}
|
||||
alignItems="center"
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.15, ease: "easeOut" }}
|
||||
>
|
||||
<MotionImage
|
||||
w={{ base: '45vw', sm: '180px', md: '80px', lg: '110px', xl: '110px' }}
|
||||
src={item.image}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.1, ease: "easeOut" }}
|
||||
/>
|
||||
|
||||
{item.id === 3 ? (
|
||||
<>
|
||||
<MotionText
|
||||
mt={{ base: '-70px', sm: '-70px', md: '-40px', lg: '-55px', xl: '-55px' }}
|
||||
className='font-melle font-xbold'
|
||||
fontWeight={400}
|
||||
color='white'
|
||||
fontSize={{ base: '9vw', sm: '4xl', md: '3xl', lg: '5xl', xl: '5xl' }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{item.title}
|
||||
</MotionText>
|
||||
<MotionImage
|
||||
mt={{ base: '-15px', sm: '-15px', md: '-15px', lg: '-20px', xl: '-20px' }}
|
||||
h={{ base: '18vw', sm: '80px', md: '60px', lg: '80px', xl: '80px' }}
|
||||
src={item.titleImage2}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.3, ease: "easeOut" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MotionImage
|
||||
mt={{ base: '-70px', sm: '-70px', md: '-30px', lg: '-50px', xl: '-50px' }}
|
||||
h={{ base: '18vw', sm: '80px', md: '60px', lg: '80px', xl: '80px' }}
|
||||
src={item.titleImage2}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.2, ease: "easeOut" }}
|
||||
/>
|
||||
|
||||
<MotionText
|
||||
mt={{ base: '-25px', sm: '-25px', md: '-15px', lg: '-25px', xl: '-25px' }}
|
||||
className='font-melle font-xbold'
|
||||
fontWeight={400}
|
||||
color='white'
|
||||
fontSize={{ base: '6vw', sm: '6vw', md: '3xl', lg: '5xl', xl: '5xl' }}
|
||||
fontSize={{ base: '9vw', sm: '5xl', md: '3xl', lg: '5xl', xl: '5xl' }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Image
|
||||
h={{ base: '40vw', sm: item.id != 4 ? '200px' : '300px', md: item.id != 4 ? '180px' : '280px', lg: item.id != 4 ? '200px' : '300px', xl: item.id != 4 ? '200px' : '300px' }}
|
||||
</MotionText>
|
||||
</>
|
||||
)}
|
||||
<MotionImage
|
||||
h={{ base: item.id != 4 ? '50vw' : '70vw', sm: item.id != 4 ? '200px' : '300px', md: item.id != 4 ? '180px' : '280px', lg: item.id != 4 ? '200px' : '300px', xl: item.id != 4 ? '200px' : '300px' }}
|
||||
src={item.titleImage}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.4, ease: "easeOut" }}
|
||||
/>
|
||||
|
||||
<Text
|
||||
w={{ base: '90%', sm: '70%', md: '80%', lg: '80%', xl: '80%' }}
|
||||
<MotionText
|
||||
w={{ base: '90%', sm: '80%', md: '80%', lg: '80%', xl: '80%' }}
|
||||
className="font-noto-sans font-demi-light"
|
||||
fontSize={{ base: 'md', sm: 'xl', md: 'xl', lg: '2xl', xl: '2xl' }}
|
||||
fontSize={{ base: '17.5px', sm: 'xl', md: 'xl', lg: '2xl', xl: '2xl' }}
|
||||
color="white"
|
||||
lineHeight="1.8"
|
||||
textAlign="left"
|
||||
whiteSpace="pre-wrap"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.5, ease: "easeOut" }}
|
||||
>
|
||||
{renderDescription(item.description)}
|
||||
</Text>
|
||||
</MotionText>
|
||||
|
||||
{item.id === 4 ? (
|
||||
<Box
|
||||
width="100%"
|
||||
display="flex"
|
||||
flexDirection={'column'}
|
||||
alignItems="center"
|
||||
alignItems="flex-start"
|
||||
ref={oilCubesRef}
|
||||
mt={-4}
|
||||
ml={3}
|
||||
>
|
||||
<Flex flexWrap="wrap" justifyContent="center" gap="10px" maxWidth="945px">
|
||||
<MotionFlex flexDirection={'row'} flexWrap="wrap" justifyContent="center" gap="10px" maxWidth="680px"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isOilCubesInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{oilCube.map((item, index) => (
|
||||
<MotionStack
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isOilCubesInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 }}
|
||||
w='300px'
|
||||
w='calc(50% - 5px)'
|
||||
minW='280px'
|
||||
h='100px'
|
||||
bgImage={item.bgColor}
|
||||
roundedTopLeft={'30px'}
|
||||
@@ -226,20 +514,20 @@ function Advantages() {
|
||||
</Text>
|
||||
</MotionStack>
|
||||
))}
|
||||
</Flex>
|
||||
</MotionFlex>
|
||||
</Box>
|
||||
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
</Stack>
|
||||
</MotionStack>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</MotionSimpleGrid>
|
||||
|
||||
|
||||
|
||||
</Box >
|
||||
</MotionBox >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
import { Box, Image, Stack } from '@chakra-ui/react'
|
||||
import { Box, Image, Stack, Text } from '@chakra-ui/react'
|
||||
import CyclingImage from './CyclingImage'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const MotionImage = motion.create(Image)
|
||||
const MotionStack = motion.create(Stack)
|
||||
const MotionText = motion.create(Text)
|
||||
function Hero1() {
|
||||
const [headerHeight, setHeaderHeight] = useState(80);
|
||||
const bigWarningSize = { base: "17vw", sm: "15vw", md: "9vw", lg: "9vw", xl: "8vw" };
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeaderHeight = () => {
|
||||
const header = document.querySelector('[data-header="true"]');
|
||||
if (header) {
|
||||
const height = header.getBoundingClientRect().height;
|
||||
setHeaderHeight(height);
|
||||
}
|
||||
};
|
||||
|
||||
const delayedUpdate = () => {
|
||||
setTimeout(updateHeaderHeight, 100);
|
||||
};
|
||||
|
||||
delayedUpdate();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setTimeout(updateHeaderHeight, 50);
|
||||
});
|
||||
|
||||
const header = document.querySelector('[data-header="true"]');
|
||||
if (header) {
|
||||
resizeObserver.observe(header);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', delayedUpdate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', delayedUpdate);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
w="100%"
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
|
||||
pt={`${headerHeight}px`}
|
||||
>
|
||||
{/* Global keyframes for all animations */}
|
||||
<style>
|
||||
@@ -59,39 +97,34 @@ function Hero1() {
|
||||
<Box
|
||||
position="relative"
|
||||
w="100%"
|
||||
minH={{ base: "110vw", sm: "110vw", md: "70vw", lg: "70vw", xl: "45vw" }}
|
||||
bgImage={{base:"url('/images/new/confuse_mobile.webp')",sm:"url('/images/new/confuse_mobile.webp')",md:"url('/images/new/confuse.webp')",lg:"url('/images/new/confuse.webp')",xl:"url('/images/new/confuse.webp')"}}
|
||||
minH={{ base: "220vw", sm: "220vw", md: "93vw", lg: "95vw", xl: "93vw" }}
|
||||
bgImage={{ base: "url('/images/new/new_hero_mobile_bg.webp')", sm: "url('/images/new/new_hero_mobile_bg.webp')", md: "url('/images/new/new_hero_pc_bg.webp')", lg: "url('/images/new/new_hero_pc_bg.webp')", xl: "url('/images/new/new_hero_pc_bg.webp')" }}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
justifyItems={'center'}
|
||||
bgRepeat="no-repeat"
|
||||
>
|
||||
{/* Hero 1 title */}
|
||||
<Image src="/images/new/hero_text1x.webp"
|
||||
<MotionImage
|
||||
src="/images/new/hero_text1x.webp"
|
||||
position={'absolute'}
|
||||
bottom={{ base: '70px', sm: '100px', md: '100px', lg: '100px', xl: '100px' }}
|
||||
maxW={{ base: "70vw", sm: "70vw", md: "50vw", lg: "50vw", xl: "35vw" }}
|
||||
left="50%"
|
||||
top={{ base: '80vw', sm: '80vw', md: '26vw', lg: '26vw', xl: '28vw' }}
|
||||
maxW={{ base: "70vw", sm: "70vw", md: "40vw", lg: "50vw", xl: "35vw" }}
|
||||
alt='hero title'
|
||||
css={{
|
||||
animation: 'fadeInUp 1s ease-out',
|
||||
'@keyframes fadeInUp': {
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(30px)'
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
initial={{ opacity: 0, y: 30, x: '-50%' }}
|
||||
whileInView={{ opacity: 1, y: 0, x: '-50%' }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
{/* Mobile arrow - shown on base and sm */}
|
||||
<CyclingImage
|
||||
src="/images/new/arrow_mobile_1.webp"
|
||||
src2="/images/new/arrow_mobile_2.webp"
|
||||
position={'absolute'}
|
||||
maxW={{ base: "70%", sm: "70%", md: "70%", lg: "70%", xl: "70%" }}
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
maxW={{ base: "90%", sm: "90%", md: "70%", lg: "70%", xl: "70%" }}
|
||||
bottom={'10px'}
|
||||
display={{ base: "block", sm: "block", md: "none", lg: "none", xl: "none" }}
|
||||
cycleDuration={0.5}
|
||||
@@ -102,12 +135,145 @@ function Hero1() {
|
||||
src="/images/new/arrow_pc_1.webp"
|
||||
src2="/images/new/arrow_pc_2.webp"
|
||||
position={'absolute'}
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
maxW={{ base: "70%", sm: "70%", md: "70%", lg: "70%", xl: "70%" }}
|
||||
bottom={'10px'}
|
||||
display={{ base: "none", sm: "none", md: "block", lg: "block", xl: "block" }}
|
||||
cycleDuration={0.5}
|
||||
intensity={0}
|
||||
/>
|
||||
|
||||
|
||||
{/* title */}
|
||||
<MotionStack
|
||||
position="absolute"
|
||||
left="50%"
|
||||
w={'100%'}
|
||||
top={{ base: '140vw', sm: '140vw', md: '60vw', lg: '60vw', xl: '58vw' }}
|
||||
alignItems={'center'}
|
||||
gap={'1px'}
|
||||
initial={{ opacity: 0, y: 50, x: '-50%' }}
|
||||
whileInView={{ opacity: 1, y: 0, x: '-50%' }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
{/* <MotionText
|
||||
className="font-melle font-xbold"
|
||||
fontSize={{ base: '15vw', sm: '13vw', md: '6vw', lg: '5vw', xl: '3.5vw' }}
|
||||
color={"#FED407"}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{'你要注意!'}
|
||||
</MotionText> */}
|
||||
<MotionText
|
||||
className="font-melle font-xbold"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '3vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"#FED407"}
|
||||
mt={{ base: -5, sm: -7, md: -5, lg: -5, xl: -5 }}
|
||||
mr={{ base: 3, sm: 3, md: 0, lg: 0, xl: 0 }}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{'身體健康響起警號!'}
|
||||
</MotionText>
|
||||
<MotionText
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '2.5vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"white"}
|
||||
mt={{ base: -2, sm: -5, md: -3, lg: -3, xl: -3 }}
|
||||
mr={4}
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
{'好多時係同'}
|
||||
</MotionText>
|
||||
|
||||
<Box display="inline" mt={{ base: -2, sm: -5, md: -3, lg: -3, xl: -3 }} mr={4}>
|
||||
<MotionText
|
||||
className="font-melle font-xbold"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '2.5vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"white"}
|
||||
display="inline"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.5, delay: 0.5 }}
|
||||
>
|
||||
{'日常飲食習慣'}
|
||||
</MotionText>
|
||||
<MotionText
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '2.5vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"white"}
|
||||
display="inline"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
>
|
||||
{'有關!'}
|
||||
</MotionText>
|
||||
</Box>
|
||||
<MotionText
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '2.5vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"white"}
|
||||
mt={-2}
|
||||
// mt={{ base: 8, sm: 7, md: 5, lg: 5, xl: 5 }}
|
||||
mr={4}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 0.7, ease: "easeOut" }}
|
||||
>
|
||||
{'簡單到喺你屋企'}
|
||||
</MotionText>
|
||||
|
||||
<MotionImage
|
||||
src="/images/new/wraning_subtitle.webp"
|
||||
w={{ base: '70vw', sm: '70vw', md: '30vw', lg: '30vw', xl: '30vw' }}
|
||||
mt={-1}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
<MotionText
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '3vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"white"}
|
||||
mt={-3}
|
||||
mr={4}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 0.9, ease: "easeOut" }}
|
||||
>
|
||||
{'其實已經日積月累'}
|
||||
</MotionText>
|
||||
<MotionText
|
||||
className="font-melle font-medium"
|
||||
fontSize={{ base: '7vw', sm: '7vw', md: '2.3vw', lg: '3vw', xl: '3vw' }}
|
||||
color={"white"}
|
||||
mt={-3}
|
||||
mr={4}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.6, delay: 1.0, ease: "easeOut" }}
|
||||
>
|
||||
{'咁影響緊你!'}
|
||||
</MotionText>
|
||||
</MotionStack>
|
||||
|
||||
{/* signs */}
|
||||
<Box
|
||||
position={'absolute'}
|
||||
@@ -130,8 +296,8 @@ function Hero1() {
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "13vw", sm: "13vw", md: "6vw", lg: "6vw", xl: "5vw" }}
|
||||
left={{ base: '6vw', sm: '6vw', md: '13vw', lg: '13vw', xl: '25vw' }}
|
||||
bottom={{ base: '50vw', sm: '50vw', md: '27vw', lg: '27vw', xl: '18vw' }}
|
||||
left={{ base: '10vw', sm: '10vw', md: '23vw', lg: '23vw', xl: '25vw' }}
|
||||
top={{ base: '50vw', sm: '50vw', md: '27vw', lg: '27vw', xl: '22vw' }}
|
||||
css={{
|
||||
animation: 'floatMedium 3.5s ease-in-out infinite',
|
||||
animationDelay: '0.5s'
|
||||
@@ -147,9 +313,9 @@ function Hero1() {
|
||||
</Box>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "13vw", sm: "13vw", md: "6vw", lg: "6vw", xl: "5vw" }}
|
||||
right={{ base: '7vw', sm: '7vw', md: '14vw', lg: '14vw', xl: '20vw' }}
|
||||
top={{ base: '60vw', sm: '60vw', md: '40vw', lg: '40vw', xl: '25vw' }}
|
||||
w={bigWarningSize}
|
||||
right={{ base: '7vw', sm: '7vw', md: '17vw', lg: '17vw', xl: '20vw' }}
|
||||
top={{ base: '60vw', sm: '60vw', md: '30vw', lg: '30vw', xl: '30vw' }}
|
||||
css={{
|
||||
animation: 'floatFast 3.2s ease-in-out infinite',
|
||||
animationDelay: '1s'
|
||||
@@ -166,8 +332,8 @@ function Hero1() {
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={bigWarningSize}
|
||||
right={{ base: '11vw', sm: '11vw', md: '18vw', lg: '18vw', xl: '18vw' }}
|
||||
top={{ base: '12vw', sm: '12vw', md: '20vw', lg: '20vw', xl: '10vw' }}
|
||||
right={{ base: '5vw', sm: '5vw', md: '18vw', lg: '18vw', xl: '18vw' }}
|
||||
top={{ base: '12vw', sm: '12vw', md: '15vw', lg: '15vw', xl: '10vw' }}
|
||||
css={{
|
||||
animation: 'floatSlow 3.3s ease-in-out infinite',
|
||||
animationDelay: '1.5s'
|
||||
@@ -184,9 +350,9 @@ function Hero1() {
|
||||
{/* Warning Texts */}
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "20vw", sm: "20vw", md: "11vw", lg: "11vw", xl: "10vw" }}
|
||||
left={{ base: "23vw", sm: "23vw", md: "30vw", lg: "30vw", xl: "37vw" }}
|
||||
top={{ base: "9vw", sm: "9vw", md: "7vw", lg: "7vw", xl: "3vw" }}
|
||||
w={{ base: "25vw", sm: "25vw", md: "11vw", lg: "11vw", xl: "10vw" }}
|
||||
left={{ base: "23vw", sm: "23vw", md: "37vw", lg: "37vw", xl: "37vw" }}
|
||||
top={{ base: "9vw", sm: "9vw", md: "4vw", lg: "4vw", xl: "3vw" }}
|
||||
css={{
|
||||
animation: 'floatMedium 3.2s ease-in-out infinite',
|
||||
animationDelay: '0.2s'
|
||||
@@ -202,9 +368,9 @@ function Hero1() {
|
||||
</Box>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "20vw", sm: "20vw", md: "10vw", lg: "10vw", xl: "9vw" }}
|
||||
left={{ base: "35vw", sm: "35vw", md: "18vw", lg: "18vw", xl: "25vw" }}
|
||||
top={{ base: "25vw", sm: "25vw", md: "20vw", lg: "20vw", xl: "14vw" }}
|
||||
w={{ base: "27vw", sm: "27vw", md: "10vw", lg: "10vw", xl: "9vw" }}
|
||||
left={{ base: "35vw", sm: "35vw", md: "23vw", lg: "23vw", xl: "25vw" }}
|
||||
top={{ base: "25vw", sm: "25vw", md: "14vw", lg: "14vw", xl: "14vw" }}
|
||||
css={{
|
||||
animation: 'floatFast 3.5s ease-in-out infinite',
|
||||
animationDelay: '0.7s'
|
||||
@@ -220,8 +386,8 @@ function Hero1() {
|
||||
</Box>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "29vw", sm: "29vw", md: "14vw", lg: "14vw", xl: "13vw" }}
|
||||
right={{ base: "20vw", sm: "20vw", md: "34vw", lg: "34vw", xl: "35vw" }}
|
||||
w={{ base: "34vw", sm: "34vw", md: "14vw", lg: "14vw", xl: "13vw" }}
|
||||
right={{ base: "15vw", sm: "15vw", md: "34vw", lg: "34vw", xl: "35vw" }}
|
||||
top={{ base: "10vw", sm: "10vw", md: "7vw", lg: "7vw", xl: "5vw" }}
|
||||
css={{
|
||||
animation: 'floatSlow 3.8s ease-in-out infinite',
|
||||
@@ -238,9 +404,9 @@ function Hero1() {
|
||||
</Box>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "13vw", sm: "13vw", md: "8vw", lg: "8vw", xl: "7vw" }}
|
||||
right={{ base: "14vw", sm: "14vw", md: "26vw", lg: "26vw", xl: "26vw" }}
|
||||
top={{ base: "35vw", sm: "35vw", md: "18vw", lg: "18vw", xl: "12vw" }}
|
||||
w={{ base: "19vw", sm: "19vw", md: "8vw", lg: "8vw", xl: "7vw" }}
|
||||
right={{ base: "10vw", sm: "10vw", md: "28vw", lg: "28vw", xl: "26vw" }}
|
||||
top={{ base: "35vw", sm: "35vw", md: "14vw", lg: "14vw", xl: "12vw" }}
|
||||
css={{
|
||||
animation: 'floatFast 3.3s ease-in-out infinite',
|
||||
animationDelay: '0.9s'
|
||||
@@ -254,6 +420,61 @@ function Hero1() {
|
||||
intensity={0.1}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={bigWarningSize}
|
||||
left={{ base: '40vw', sm: '40vw', md: '45vw', lg: '45vw', xl: '45vw' }}
|
||||
top={{ base: '120vw', sm: '120vw', md: '51vw', lg: '51vw', xl: '50vw' }}
|
||||
css={{
|
||||
animation: 'floatSlow 3s ease-in-out infinite',
|
||||
animationDelay: '0s'
|
||||
}}
|
||||
>
|
||||
<CyclingImage
|
||||
src="/images/new/bigwarning.webp"
|
||||
position={'relative'}
|
||||
w="100%"
|
||||
cycleDuration={3}
|
||||
intensity={0.1}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "8vw", sm: "8vw", md: "6vw", lg: "6vw", xl: "5vw" }}
|
||||
right={{ base: '6vw', sm: '6vw', md: '25vw', lg: '25vw', xl: '25vw' }}
|
||||
bottom={{ base: '55vw', sm: '55vw', md: '18vw', lg: '18vw', xl: '18vw' }}
|
||||
css={{
|
||||
animation: 'floatMedium 3.5s ease-in-out infinite',
|
||||
animationDelay: '0.5s'
|
||||
}}
|
||||
>
|
||||
<CyclingImage
|
||||
src="/images/new/bigwarning.webp"
|
||||
position={'relative'}
|
||||
w="100%"
|
||||
cycleDuration={3}
|
||||
intensity={0.1}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
w={{ base: "8vw", sm: "8vw", md: "6vw", lg: "6vw", xl: "5vw" }}
|
||||
left={{ base: '6vw', sm: '6vw', md: '25vw', lg: '25vw', xl: '25vw' }}
|
||||
bottom={{ base: '70vw', sm: '70vw', md: '24vw', lg: '24vw', xl: '24vw' }}
|
||||
css={{
|
||||
animation: 'floatMedium 3.5s ease-in-out infinite',
|
||||
animationDelay: '0.5s'
|
||||
}}
|
||||
>
|
||||
<CyclingImage
|
||||
src="/images/new/bigwarning.webp"
|
||||
position={'relative'}
|
||||
w="100%"
|
||||
cycleDuration={3}
|
||||
intensity={0.1}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -13,10 +13,10 @@ function Hero2() {
|
||||
|
||||
>
|
||||
<Stack gap={0}>
|
||||
<Box
|
||||
<Stack
|
||||
position="relative"
|
||||
w="100%"
|
||||
minH={{ base: "115vw", sm: "105vw", md: "90vw", lg: "73vw", xl: "65vw" }}
|
||||
minH={{ base: "130vw", sm: "130vw", md: "72vw", lg: "66vw", xl: "50vw" }}
|
||||
bgImage={"url('/images/new/hero2bg.webp')"}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
@@ -28,10 +28,11 @@ function Hero2() {
|
||||
<MotionImage
|
||||
src="/images/new/hero2title.webp"
|
||||
position={'absolute'}
|
||||
top={{ base: "10px", sm: "50px", md: "50px", lg: "50px", xl: "50px" }}
|
||||
w={{ base: "70vw", sm: "60vw", md: "50vw", lg: "35vw", xl: "35vw" }}
|
||||
initial={{ opacity: 0, y: -30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
left="50%"
|
||||
top={{ base: "10px", sm: "20px", md: "30px", lg: "30px", xl: "40px" }}
|
||||
w={{ base: "70vw", sm: "70vw", md: "35vw", lg: "35vw", xl: "28vw" }}
|
||||
initial={{ opacity: 0, y: -30, x: '-50%' }}
|
||||
whileInView={{ opacity: 1, y: 0, x: '-50%' }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
/>
|
||||
@@ -39,9 +40,9 @@ function Hero2() {
|
||||
src="/images/new/hero2subtitle.webp"
|
||||
position={'absolute'}
|
||||
zIndex={1}
|
||||
top={{ base: "30vw", sm: "35vw", md: "24vw", lg: "20vw", xl: "18vw" }}
|
||||
right={{ base: "15vw", sm: "15vw", md: "20vw", lg: "28vw", xl: "30vw" }}
|
||||
w={{ base: "30vw", sm: "30vw", md: "25vw", lg: "18vw", xl: "18vw" }}
|
||||
top={{ base: "35vw", sm: "35vw", md: "17vw", lg: "15vw", xl: "12vw" }}
|
||||
right={{ base: "55vw", sm: "55vw", md: "24vw", lg: "25vw", xl: "30vw" }}
|
||||
w={{ base: "34vw", sm: "34vw", md: "22vw", lg: "22vw", xl: "18vw" }}
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
@@ -49,22 +50,23 @@ function Hero2() {
|
||||
/>
|
||||
|
||||
<MotionImage
|
||||
src="/images/new/oil.webp"
|
||||
src="/images/new/oil_new.webp"
|
||||
zIndex={0}
|
||||
position={'absolute'}
|
||||
bottom={{ base: -10, sm: -20, md: -20, lg: -20, xl: -20 }}
|
||||
w={{ base: "80vw", sm: "70vw", md: "60vw", lg: "50vw", xl: "40vw" }}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
left="50%"
|
||||
bottom={0}
|
||||
w={{ base: "150vw", sm: "150vw", md: "58vw", lg: "53vw", xl: "40vw" }}
|
||||
initial={{ opacity: 0, y: 50, x: '-50%' }}
|
||||
whileInView={{ opacity: 1, y: 0, x: '-50%' }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
|
||||
</Box>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
export default Hero2;
|
||||
@@ -11,19 +11,20 @@ interface InfoData {
|
||||
title: string
|
||||
image: string
|
||||
description: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const infoData: InfoData[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '膽固醇\n超標',
|
||||
image: '/images/new/info_1.webp',
|
||||
description: `長期攝取過多**飽和脂肪及反式脂肪(例如牛油、加工食品、或重複使用嘅煎炸油)**,會令體內**壞膽固醇(LDL)上升、好膽固醇(HDL)下降**,導致膽固醇積聚喺血管內壁,形成血塊堵塞血管,有機會引致**中風、心肌梗塞,**甚至**心臟病**等嚴重後果。`
|
||||
image: '/images/new/blood.webp',
|
||||
description: `長期攝取過多**飽和脂肪及反式脂肪(如牛油、加工食品、或重複使用嘅煎炸油)**,會令體內**壞膽固醇(LDL)上升、好膽固醇(HDL)下降**,導致膽固醇積聚喺血管內壁,形成血塊堵塞血管,有機會引致**中風、心肌梗塞,**甚至**心臟病**等嚴重後果。`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '中央肥胖\n脂肪肝',
|
||||
image: '/images/new/info_2.webp',
|
||||
image: '/images/new/liver.webp',
|
||||
description: `研究指出,將煎炸油長時間加熱至超過煙點或重複使用,會令**油質氧化,**產生聚合物及自由基。前者會令油變黏稠、有油膩味,**增加肝臟負擔;**後者會攻擊細胞,加速身體衰老並提升患上慢性病嘅風險。
|
||||
|
||||
長期攝取呢類油脂,會干擾身體脂肪代謝速度,**令過多脂肪積聚喺肝臟,**並削弱其解毒功能,令毒素累積喺體內,最終導致**脂肪肝**。如果肝臟持續受損,可能就會留下永久損傷,最後導致**肝硬化**。`
|
||||
@@ -31,7 +32,7 @@ const infoData: InfoData[] = [
|
||||
{
|
||||
id: 3,
|
||||
title: '皮膚暗啞\n容易脫髮',
|
||||
image: '/images/new/info_3.webp',
|
||||
image: '/images/new/skin.webp',
|
||||
description: `好多人以為「滴油不沾」先健康,但其實**持續攝取適量又優質嘅油脂,**先可以促進毛髮健康生長,同時令皮膚保持彈性同水潤。
|
||||
|
||||
因為**脂肪係製造荷爾蒙**嘅重要原素,而脂溶性維他命A、D、E、K亦必須喺有油嘅情況下先可以被吸收,長遠先能真正發揮對皮膚、頭髮同整體健康嘅作用,呈現由內而外嘅自然光澤。`
|
||||
@@ -39,10 +40,10 @@ const infoData: InfoData[] = [
|
||||
{
|
||||
id: 4,
|
||||
title: '經常容易\n覺得疲倦',
|
||||
image: '/images/new/info_4.webp',
|
||||
description: `香港人生活節奏急促,一日三餐唔係叫外賣就出街食,而餐廳普遍使用 Omega-6 偏高嘅食油(如大豆油)。**長期攝取會令體內Omega-6遠多於Omega-3,脂肪酸比例失衡,促進身體慢性發炎。**
|
||||
image: '/images/new/brain.webp',
|
||||
description: `香港人生活節奏急促,一日三餐唔係叫外賣就出街食,而餐廳普遍使用 Omega-6 偏高嘅食油(如大豆油)。**長期攝取會令體內Omega-6遠多於Omega-3,脂肪酸比例失衡,促進身體慢性發炎。**
|
||||
|
||||
失衡嘅狀況如果未能得到改善,會慢慢出現容易攰、記憶力下降、專注力不足等情況;若持續惡化,仲可能影響腦部神經傳導,進一步提升患上**腦霧及失智症**嘅風險。 `
|
||||
如果失衡未能改善,尤其Omega-3長期攝取不足,就容易出現易攰、記憶力下降、專注力不足等情況。隨住狀況持續惡化,仲可能影響腦部神經傳導,提升**腦霧同失智症**風險。`
|
||||
}
|
||||
]
|
||||
|
||||
@@ -66,6 +67,7 @@ function Info() {
|
||||
|
||||
return (
|
||||
<Box
|
||||
id="info"
|
||||
position="relative"
|
||||
w="100%"
|
||||
minH={{ base: "163vw", sm: "163vw", md: "75vw", lg: "50vw", xl: "45vw" }}
|
||||
@@ -103,7 +105,7 @@ function Info() {
|
||||
cursor="pointer"
|
||||
onClick={() => setSelectedInfo(info.id)}
|
||||
bg={selectedInfo === info.id ? '#3D6741' : '#BCBCBC'}
|
||||
border={selectedInfo === info.id ? "4px solid #99BF35" : "none"}
|
||||
border={selectedInfo === info.id ? { base: "1px solid #99BF35", sm: "1px solid #99BF35", md: "4px solid #99BF35", lg: "4px solid #99BF35", xl: "4px solid #99BF35" } : "none"}
|
||||
borderRadius={{ base: '15px', sm: '15px', md: '18px', lg: '20px', xl: '20px' }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
@@ -111,13 +113,13 @@ function Info() {
|
||||
textAlign="center"
|
||||
fontFamily="'MElleHK', sans-serif"
|
||||
fontWeight={selectedInfo === info.id ? 800 : 500}
|
||||
fontSize={{ base: '3.5vw', sm: '19px', md: '2xl', lg: '2xl', xl: '2xl' }}
|
||||
fontSize={{ base: '3.8vw', sm: '19px', md: '2xl', lg: '2xl', xl: '2xl' }}
|
||||
color={selectedInfo === info.id ? '#FFFFFF' : '#DDDDDD'}
|
||||
whiteSpace="pre-line"
|
||||
lineHeight="1.2"
|
||||
transition="all 0.3s ease"
|
||||
px={2}
|
||||
boxShadow={info.id === selectedInfo ? "inset 9px 9px 9px rgba(0,0,0,0.25), 4px 4px 10px rgba(0,0,0,0.45)" : info.id === selectedInfo + 1 ? "inset 9px 3px 9px rgba(0,0,0,0.3)" : "none"}
|
||||
boxShadow={info.id === selectedInfo ? "inset 3px 3px 3px rgba(0,0,0,0.25), 4px 4px 10px rgba(0,0,0,0.45)" : info.id === selectedInfo + 1 ? "inset 3px 3px 3px rgba(0,0,0,0.25)" : "none"}
|
||||
>
|
||||
{info.title}
|
||||
</Box>
|
||||
@@ -147,7 +149,7 @@ function Info() {
|
||||
pb={{ base: 5, sm: 5, md: 6, lg: 9, xl: 9 }}>
|
||||
<Text
|
||||
className="font-noto-sans font-regular"
|
||||
fontSize={{ base: 'md', sm: 'lg', md: 'xl', lg: '2xl', xl: '2xl' }}
|
||||
fontSize={{ base: 'xl', sm: 'xl', md: 'xl', lg: '2xl', xl: '2xl' }}
|
||||
color="#3D6741"
|
||||
lineHeight="1.8"
|
||||
textAlign="left"
|
||||
|
||||
98
src/components/new_ui/oilInfo.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Box, Image, SimpleGrid } from '@chakra-ui/react'
|
||||
import { motion, useInView } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
import {useRouterState } from '@tanstack/react-router'
|
||||
|
||||
const MotionImage = motion.create(Image)
|
||||
const MotionBox = motion.create(Box)
|
||||
const MotionSimpleGrid = motion.create(SimpleGrid)
|
||||
|
||||
function OilInfo() {
|
||||
const mainRef = useRef(null);
|
||||
const isMainInView = useInView(mainRef, { once: true });
|
||||
const { location } = useRouterState();
|
||||
|
||||
const handleRecipeNavigation = () => {
|
||||
const isOnRecipePage = location.pathname === '/40plus/recipe';
|
||||
|
||||
if (isOnRecipePage) {
|
||||
// Already on recipe page
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate from main page - force reload to ensure Instagram embeds work
|
||||
window.location.href = '/40plus/recipe';
|
||||
};
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
ref={mainRef}
|
||||
position="relative"
|
||||
w="100%"
|
||||
bgColor="#FFFBD3"
|
||||
zIndex={3}
|
||||
overflow="hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isMainInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
|
||||
<MotionSimpleGrid
|
||||
columns={{ base: 1, sm: 1, md: 2 }}
|
||||
p={{ base: 10, sm: 20, md: 20 }}
|
||||
w={{ base: '100%', sm: '100%', md: '100%', lg: '70%', xl: '60%' }}
|
||||
h={'auto'}
|
||||
gap={{ base: 10, md: 0 }}
|
||||
mx="auto"
|
||||
justifyItems={'center'}
|
||||
alignItems={'center'}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
|
||||
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
|
||||
<MotionBox
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<MotionImage
|
||||
src='/images/cookmethods.webp'
|
||||
alt='cooking methods'
|
||||
w={{ base: '100%', sm: '90%', md: '85%' }}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: 0.6, ease: "easeOut" }}
|
||||
/>
|
||||
</MotionBox>
|
||||
|
||||
<MotionBox
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: 50 }}
|
||||
transition={{ duration: 0.6, delay: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
<MotionImage
|
||||
src='/images/new/buttons.webp'
|
||||
w={{ base: '100%', sm: '100%', md: '90%' }}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: 1.0, ease: "easeOut" }}
|
||||
cursor="pointer"
|
||||
onClick={handleRecipeNavigation}
|
||||
/>
|
||||
</MotionBox>
|
||||
|
||||
|
||||
</MotionSimpleGrid>
|
||||
|
||||
</MotionBox>
|
||||
)
|
||||
}
|
||||
|
||||
export default OilInfo;
|
||||
312
src/components/new_ui/truth.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import { Box, HStack, Image, Stack, Text, Link, Flex } from '@chakra-ui/react'
|
||||
import { motion, useInView } from 'framer-motion'
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const MotionBox = motion.create(Box)
|
||||
const MotionStack = motion.create(Stack)
|
||||
const MotionHStack = motion.create(HStack)
|
||||
const MotionText = motion.create(Text)
|
||||
const MotionImage = motion.create(Image)
|
||||
const MotionFlex = motion.create(Flex)
|
||||
const truthItems = [
|
||||
|
||||
{
|
||||
id: 1,
|
||||
image: '/images/new/ans_step1.webp',
|
||||
desc: '**出街食飯死亡率大增近五成!**即睇營養師拆解「油」之迷思',
|
||||
url: 'https://www.hk01.com/%E5%81%A5%E5%BA%B7Easy/60225791/%E5%87%BA%E8%A1%97%E9%A3%9F%E9%A3%AF%E6%AD%BB%E4%BA%A1%E7%8E%87%E5%A4%A7%E5%A2%9E%E8%BF%91%E4%BA%94%E6%88%90-%E5%8D%B3%E7%9D%87%E7%87%9F%E9%A4%8A%E5%B8%AB%E6%8B%86%E8%A7%A3-%E6%B2%B9-%E4%B9%8B%E8%BF%B7%E6%80%9D'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: '/images/new/ans_step2.webp',
|
||||
desc: '**三高年輕化警號!**營養師揭港人隱形致肥陷阱!一支好油決定血管命運',
|
||||
url: 'https://urbanlifehk.com/article/128710/%E4%B8%89%E9%AB%98-%E8%87%B4%E8%82%A5'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: '/images/new/ans_step3.webp',
|
||||
desc: '**三高人士同樣啱食!**營養師教你自製健康手撕蔥油雞髀',
|
||||
url: 'https://cook1cook.com/posts/29979?srsltid=AfmBOoqWHlmsj4Sk76Hkag0Mp7lMIM_gbFbavrm-q62FwT-GVM4F4lVN'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
image: '/images/new/ans_step4.webp',
|
||||
desc: '**BBC認證豬油是健康食材?**混合食油等於垃圾油?純正食油一定最好? 各種食油迷思可能正在誤導你,由營養學家為你拆解多數人不知道的真相!',
|
||||
url: 'https://www.orientalsunday.hk/%E7%94%9F%E6%B4%BB%E5%81%A5%E5%BA%B7/bbc%E8%AA%8D%E8%AD%89-%E5%81%A5%E5%BA%B7-%E6%B7%B7%E5%90%88%E9%A3%9F%E6%B2%B9-%E5%9E%83%E5%9C%BE%E6%B2%B9-1635198/'
|
||||
},
|
||||
]
|
||||
|
||||
const renderDescription = (text: string) => {
|
||||
const parts = text.split(/(\*\*.*?\*\*)/)
|
||||
|
||||
return parts.map((part, index) => {
|
||||
if (part.startsWith('**') && part.endsWith('**')) {
|
||||
return (
|
||||
<Text as="span" key={index} fontWeight={800}>
|
||||
{part.slice(2, -2)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return <Text as="span" key={index}>{part}</Text>
|
||||
})
|
||||
}
|
||||
|
||||
function Truth() {
|
||||
|
||||
const shopImages = [
|
||||
{
|
||||
image: '/images/pp.webp',
|
||||
link: 'https://www.pns.hk/zh-hk/%E7%87%9F%E8%90%83%E8%AD%B7%E5%BF%83%E6%B2%B9/p/BP_401649',
|
||||
},
|
||||
{
|
||||
image: '/images/we.webp',
|
||||
link: 'https://www.wellcome.com.hk/zh-hant/p/%E7%8D%85%E7%90%83%E5%98%9C%E7%87%9F%E8%90%83%E8%AD%B7%E5%BF%83%E6%B2%B9%201%E5%85%AC%E5%8D%87/i/101341721.html?srsltid=AfmBOorpxphAzws9TIkKJW5xr1pYOaUOL5kHbsgRjYqwQeaOhq1cPQuO',
|
||||
},
|
||||
{
|
||||
image: '/images/hk.webp',
|
||||
link: 'https://www.hktvmall.com/hktv/zh/main/%E7%8D%85%E7%90%83%E5%98%9C%E6%97%97%E8%89%A6%E5%BA%97/s/H1261001/%E8%B6%85%E7%B4%9A%E5%B7%BF%E5%A0%B4/%E8%B6%85%E7%B4%9A%E5%B8%82%E5%A0%B4/%E7%B1%B3-%E9%A3%9F%E6%B2%B9/%E9%A3%9F%E6%B2%B9/%E5%85%B6%E4%BB%96/%E7%87%9F%E8%90%83%E8%AD%B7%E5%BF%83%E6%B2%B9%E9%97%9C%E6%B3%A8%E4%B8%89%E9%AB%98%E5%BF%83%E8%A1%80%E7%AE%A1%E5%81%A5%E5%BA%B7%E9%BB%83%E9%87%91%E6%AF%94%E4%BE%8B%E8%84%82%E8%82%AA%E9%85%B8%E5%B0%88%E6%A5%AD%E5%81%A5%E5%BA%B7%E9%85%8D%E6%96%B9%E9%99%8D%E5%A3%9E%E8%86%BD%E5%9B%BA%E9%86%87%E9%9B%B6%E5%8F%8D%E5%BC%8F%E8%84%82%E8%82%AA%E7%85%99%E9%BB%9E%E9%AB%98%E7%85%8E%E7%82%92%E7%85%AE%E7%82%B8/p/H0888001_S_10159486',
|
||||
},
|
||||
]
|
||||
|
||||
const fbContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const isMainInView = useInView(mainRef, { once: true });
|
||||
const isTitleInView = useInView(titleRef, { once: true });
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
|
||||
// Load Facebook SDK and initialize the plugin
|
||||
useEffect(() => {
|
||||
// Add Facebook SDK if it doesn't exist
|
||||
if (!document.getElementById('facebook-jssdk')) {
|
||||
const script = document.createElement('script');
|
||||
script.id = 'facebook-jssdk';
|
||||
script.src = 'https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v18.0';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
// Parse XFBML when SDK is loaded
|
||||
const handleSDKLoad = () => {
|
||||
if ((window as any).FB && fbContainerRef.current) {
|
||||
(window as any).FB.XFBML.parse(fbContainerRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if FB is already loaded
|
||||
if ((window as any).FB) {
|
||||
handleSDKLoad();
|
||||
} else {
|
||||
// Listen for SDK load
|
||||
(window as any).fbAsyncInit = handleSDKLoad;
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Clean up
|
||||
(window as any).fbAsyncInit = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
id="truth"
|
||||
ref={mainRef}
|
||||
position="relative"
|
||||
w="100%"
|
||||
bgColor="white"
|
||||
zIndex={3}
|
||||
overflow="hidden"
|
||||
justifyItems={'center'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isMainInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
>
|
||||
|
||||
<MotionText
|
||||
className='font-melle font-xbold'
|
||||
color='#458B02'
|
||||
mt={{ base: 10, sm: 10, md: 10, lg: 10, xl: 10 }}
|
||||
fontSize={{ base: '2xl', sm: '3xl', md: '5xl', lg: '5xl', xl: '5xl' }}
|
||||
textAlign={'center'}
|
||||
initial={{ opacity: 0, y: -30 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: -30 }}
|
||||
transition={{ duration: 0.5, delay: 0.1, ease: "easeOut" }}
|
||||
>
|
||||
想知道更多食用油健康真相?
|
||||
</MotionText>
|
||||
|
||||
<MotionStack
|
||||
mx="auto"
|
||||
mt={{ base: 3, sm: -5, md: -5, lg: -5, xl: -5 }}
|
||||
columns={{ base: 1, sm: 1, md: 2 }}
|
||||
p={{ base: 5, sm: 12, md: 20 }}
|
||||
w={{ base: '100%', sm: '100%', md: '80%', lg: '80%', xl: '60%' }}
|
||||
h={'auto'}
|
||||
gap={{ base: 7, md: 7 }}
|
||||
justifyItems={'center'}
|
||||
alignItems={'flex-start'}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
|
||||
transition={{ duration: 0.5, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{truthItems.map((item, index) => (
|
||||
<MotionHStack
|
||||
key={item.id}
|
||||
gap={{ base: 4, md: 5 }}
|
||||
justifyItems={'flex-start'}
|
||||
alignItems={'flex-start'}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: -30 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 + index * 0.1, ease: "easeOut" }}
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
const url = item.url;
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}}
|
||||
onHoverStart={() => setHoveredIndex(index)}
|
||||
onHoverEnd={() => setHoveredIndex(null)}
|
||||
>
|
||||
<MotionImage
|
||||
src={item.image}
|
||||
alt={`truth item ${item.id}`}
|
||||
w={{ base: '70px', sm: '70px', md: '70px', lg: '70px', xl: '70px' }}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? (hoveredIndex === index ?
|
||||
{ opacity: 1, scale: 1.2 } :
|
||||
{
|
||||
opacity: 1,
|
||||
scale: [1, 1.05, 1],
|
||||
transition: {
|
||||
scale: {
|
||||
duration: 2.5,
|
||||
ease: "easeInOut",
|
||||
repeat: Infinity,
|
||||
repeatType: "loop"
|
||||
}
|
||||
}
|
||||
}
|
||||
) : { opacity: 0, scale: 0.7 }}
|
||||
transition={{ duration: 0.4, delay: 0.4 + index * 0.1, ease: "easeOut" }}
|
||||
/>
|
||||
<MotionText
|
||||
className="font-noto-sans font-regular"
|
||||
color='#458B02'
|
||||
fontSize={{ base: 'lg', sm: 'lg', md: 'lg', lg: 'lg', xl: 'lg' }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isMainInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.5, delay: 0.5 + index * 0.1, ease: "easeOut" }}
|
||||
>
|
||||
{renderDescription(item.desc)}
|
||||
</MotionText>
|
||||
|
||||
</MotionHStack>
|
||||
|
||||
))}
|
||||
</MotionStack>
|
||||
<motion.div
|
||||
ref={titleRef}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={isTitleInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
|
||||
transition={{ duration: 0.7 }}
|
||||
>
|
||||
<MotionStack
|
||||
w='100%'
|
||||
mt={5}
|
||||
justify={'center'}
|
||||
align={'center'}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isTitleInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.5, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
<MotionText
|
||||
className='font-melle font-xbold'
|
||||
color={"#458B02"}
|
||||
fontSize={{ base: '2xl', sm: '3xl', md: '5xl', lg: '5xl', xl: '5xl' }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isTitleInView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
點擊以下銷售點連結
|
||||
</MotionText>
|
||||
</MotionStack>
|
||||
</motion.div>
|
||||
|
||||
<MotionFlex
|
||||
gap={12}
|
||||
direction={{ base: 'column', sm: 'column', md: 'row' }}
|
||||
mt={5}
|
||||
w="100%"
|
||||
justify={{ base: 'center', sm: 'center', md: 'center' }}
|
||||
align={{ base: 'center', sm: 'center', md: 'center' }}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isTitleInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{shopImages.map((image, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isTitleInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: 0.5 + index * 0.2, ease: "easeOut" }}
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<MotionStack
|
||||
w={{ base: '250px', sm: '250px', md: '220px', lg: '250px', xl: '250px' }}
|
||||
justify={'center'}
|
||||
align={'center'}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isTitleInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.4, delay: 0.6 + index * 0.2, ease: "easeOut" }}
|
||||
>
|
||||
<Link href={image.link} style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<MotionImage src={image.image} cursor="pointer" fit={'contain'}
|
||||
loading="lazy"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isTitleInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.3, delay: 0.7 + index * 0.2, ease: "easeOut" }}
|
||||
/>
|
||||
</Link>
|
||||
</MotionStack>
|
||||
</motion.div>
|
||||
))}
|
||||
</MotionFlex>
|
||||
|
||||
|
||||
<MotionStack
|
||||
my={15}
|
||||
ml={2}
|
||||
mt={10}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isTitleInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.6, delay: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<Link href={"https://www.facebook.com/profile.php?id=100064320806613"}
|
||||
_focus={{ outline: 'none', boxShadow: 'none' }}
|
||||
_active={{ outline: 'none', boxShadow: 'none' }}>
|
||||
<MotionImage
|
||||
position="relative"
|
||||
left="50%"
|
||||
src={'/images/fb.webp'}
|
||||
alt="salespoint"
|
||||
w={"400px"}
|
||||
loading='lazy'
|
||||
initial={{ opacity: 0, y: 20, x: '-50%' }}
|
||||
animate={isTitleInView ? { opacity: 1, y: 0, x: '-50%' } : { opacity: 0, y: 20, x: '-50%' }}
|
||||
transition={{ duration: 0.5, delay: 0.6, ease: "easeOut" }}
|
||||
/>
|
||||
</Link>
|
||||
</MotionStack>
|
||||
|
||||
</MotionBox>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Truth;
|
||||
@@ -28,7 +28,6 @@ function Wraning() {
|
||||
bgImage={{ base: "url('/images/new/wraning_bg_mobile.webp')", sm: "url('/images/new/wraning_bg_mobile.webp')", md: "url('/images/new/wraning_bg.webp')", lg: "url('/images/new/wraning_bg.webp')", xl: "url('/images/new/wraning_bg.webp')" }}
|
||||
bgSize="cover"
|
||||
backgroundPosition="center"
|
||||
justifyItems={'center'}
|
||||
bgRepeat="no-repeat"
|
||||
zIndex={3}
|
||||
overflow="hidden"
|
||||
@@ -146,17 +145,19 @@ function Wraning() {
|
||||
{/* title */}
|
||||
<MotionStack
|
||||
position="absolute"
|
||||
left="50%"
|
||||
w = {'100%'}
|
||||
top={{ base: '55vw', sm: '60vw', md: '35vw', lg: '23vw', xl: '20vw' }}
|
||||
alignItems={'center'}
|
||||
gap={'1px'}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0, y: 50, x: '-50%' }}
|
||||
whileInView={{ opacity: 1, y: 0, x: '-50%' }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
<MotionText
|
||||
className="font-melle font-xbold"
|
||||
fontSize={{ base: '15vw', sm: '15vw', md: '6vw', lg: '5vw', xl: '3.5vw' }}
|
||||
fontSize={{ base: '15vw', sm: '13vw', md: '6vw', lg: '5vw', xl: '3.5vw' }}
|
||||
color={"#FED407"}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
|
||||
87
src/components/oldFooter.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Stack, Flex, Image, Text, Box } from '@chakra-ui/react';
|
||||
import { colors } from '../colors';
|
||||
|
||||
const footerText = `*獅球嘜品牌產品系列中首次推出之護心食用油
|
||||
+根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製,配方以每日25%的總攝取能量計算
|
||||
#每食用分量含0克反式脂肪
|
||||
▲Omega-6不飽和脂肪酸在體內代謝會產生花生四烯酸,進而產生促發炎的前列腺素,導致血管收縮及慢性發炎
|
||||
▼營萃護心油的omega-3比例較一般花生油、粟米油高,特別有助平衡外出用餐多的都市人其身體脂肪酸比例,增加抗發炎,維持心血管健康`
|
||||
function OldFooter() {
|
||||
|
||||
const formatText = (text: string) => {
|
||||
return text.split('\n').map((line, i) => (
|
||||
<Box key={i}
|
||||
className='font-noto-sans font-regular'
|
||||
color={colors.textColor}
|
||||
fontSize={'10px'}
|
||||
ml={2}
|
||||
>
|
||||
{line.charAt(0).match(/[\*\^\▲\+\▼\#]/) ? (
|
||||
<>
|
||||
<Text as="span" fontSize="6px" verticalAlign="super">{line.charAt(0)}</Text>
|
||||
{line.substring(1).split('\t').map((segment, j) => (
|
||||
j === 0 ? segment : <Text as="span" ml={4} key={j}>{segment}</Text>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
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>
|
||||
));
|
||||
};
|
||||
const currentYear = new Date().getFullYear();
|
||||
return (
|
||||
<Stack bg={colors.topBarColor} py={5} w="full"
|
||||
align={'center'}
|
||||
>
|
||||
<Text
|
||||
className='font-melle font-xbold'
|
||||
fontSize={'4xl'}
|
||||
color={'white'}
|
||||
>
|
||||
{"查詢:29437810"}
|
||||
</Text>
|
||||
<Flex alignItems={'center'} justifyContent={'center'} direction="column">
|
||||
<Image src="/images/header_logo.webp" alt="Logo" width="150px" />
|
||||
|
||||
</Flex>
|
||||
<Flex
|
||||
alignItems={'flex-end'}
|
||||
w={{ base: '100%', sm: '100%', md: '60%', lg: '55%', xl: '55%' }}
|
||||
h={{ base: '180px', sm: '180px', md: '130px' }}
|
||||
|
||||
>
|
||||
|
||||
<Stack gap={0}
|
||||
position={'absolute'}
|
||||
bottom={1}
|
||||
>
|
||||
{formatText(footerText)}
|
||||
<Text
|
||||
className='font-noto-sans font-regular'
|
||||
color={colors.textColor}
|
||||
fontSize={'10px'}
|
||||
mt={2}
|
||||
ml={2}
|
||||
>
|
||||
{`Copyright © ${currentYear}合興食油(香港)有限公司 版權所有,不得轉載。`}
|
||||
</Text>
|
||||
{/* <Text
|
||||
className='font-noto-sans font-regular'
|
||||
color={colors.textColor}
|
||||
fontSize={'8px'}
|
||||
|
||||
ml={2}
|
||||
>
|
||||
{`參考資料 | 台灣營養師張益堯在節目《奕起聊健康》上分享關於「油脂」的健康 https://bit.ly/3FmZkCo https://bit.ly/4htQ9gt`}
|
||||
</Text> */}
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default OldFooter;
|
||||
@@ -7,15 +7,15 @@ function Salespoint() {
|
||||
const shopImages = [
|
||||
{
|
||||
image: '/images/pp.webp',
|
||||
link: 'https://www.pns.hk/en/all-brands/b/112546/lion-globe',
|
||||
link: 'https://www.pns.hk/zh-hk/%E7%87%9F%E8%90%83%E8%AD%B7%E5%BF%83%E6%B2%B9/p/BP_401649',
|
||||
},
|
||||
{
|
||||
image: '/images/we.webp',
|
||||
link: 'https://www.wellcome.com.hk/zh-hant/search?keyword=%E7%8D%85%E7%90%83%E5%98%9C&page=1',
|
||||
link: 'https://www.wellcome.com.hk/zh-hant/p/%E7%8D%85%E7%90%83%E5%98%9C%E7%87%9F%E8%90%83%E8%AD%B7%E5%BF%83%E6%B2%B9%201%E5%85%AC%E5%8D%87/i/101341721.html?srsltid=AfmBOorpxphAzws9TIkKJW5xr1pYOaUOL5kHbsgRjYqwQeaOhq1cPQuO',
|
||||
},
|
||||
{
|
||||
image: '/images/hk.webp',
|
||||
link: 'https://www.hktvmall.com/hktv/zh/main/%E7%8D%85%E7%90%83%E5%98%9C%E6%97%97%E8%89%A6%E5%BA%97/s/H1261001',
|
||||
link: 'https://www.hktvmall.com/hktv/zh/main/%E7%8D%85%E7%90%83%E5%98%9C%E6%97%97%E8%89%A6%E5%BA%97/s/H1261001/%E8%B6%85%E7%B4%9A%E5%B7%BF%E5%A0%B4/%E8%B6%85%E7%B4%9A%E5%B8%82%E5%A0%B4/%E7%B1%B3-%E9%A3%9F%E6%B2%B9/%E9%A3%9F%E6%B2%B9/%E5%85%B6%E4%BB%96/%E7%87%9F%E8%90%83%E8%AD%B7%E5%BF%83%E6%B2%B9%E9%97%9C%E6%B3%A8%E4%B8%89%E9%AB%98%E5%BF%83%E8%A1%80%E7%AE%A1%E5%81%A5%E5%BA%B7%E9%BB%83%E9%87%91%E6%AF%94%E4%BE%8B%E8%84%82%E8%82%AA%E9%85%B8%E5%B0%88%E6%A5%AD%E5%81%A5%E5%BA%B7%E9%85%8D%E6%96%B9%E9%99%8D%E5%A3%9E%E8%86%BD%E5%9B%BA%E9%86%87%E9%9B%B6%E5%8F%8D%E5%BC%8F%E8%84%82%E8%82%AA%E7%85%99%E9%BB%9E%E9%AB%98%E7%85%8E%E7%82%92%E7%85%AE%E7%82%B8/p/H0888001_S_10159486',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -152,7 +152,7 @@ function Salespoint() {
|
||||
_focus={{ outline: 'none', boxShadow: 'none' }}
|
||||
_active={{ outline: 'none', boxShadow: 'none' }}>
|
||||
<Image
|
||||
src={'/images/facebook.webp'}
|
||||
src={'/images/fb.webp'}
|
||||
alt="salespoint"
|
||||
w={"400px"}
|
||||
loading='lazy'
|
||||
|
||||
@@ -6,16 +6,24 @@ interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
offset?: ChakraDrawer.ContentProps["padding"]
|
||||
positionerProps?: React.ComponentProps<typeof ChakraDrawer.Positioner>
|
||||
}
|
||||
|
||||
export const DrawerContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DrawerContentProps
|
||||
>(function DrawerContent(props, ref) {
|
||||
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||
const {
|
||||
children,
|
||||
portalled = true,
|
||||
portalRef,
|
||||
offset,
|
||||
positionerProps,
|
||||
...rest
|
||||
} = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraDrawer.Positioner padding={offset}>
|
||||
<ChakraDrawer.Positioner padding={offset} {...positionerProps}>
|
||||
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDrawer.Content>
|
||||
|
||||
@@ -6,10 +6,11 @@ import Qa from '../components/qa'
|
||||
import Oil_info from '../components/oil_info'
|
||||
import Bestoil from '../components/bestoil'
|
||||
import Salespoint from '../components/salespoint'
|
||||
|
||||
import Header2 from '@/components/header2'
|
||||
function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Header2 />
|
||||
<Hero1 />
|
||||
<Box mt={{ base: "-5vw", sm: "-5vw", md: "-5vw", lg: "-5vw", xl: "-5vw" }}>
|
||||
<Hero2 />
|
||||
|
||||
@@ -1,21 +1,97 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useRouterState } from '@tanstack/react-router'
|
||||
import Hero1 from '../components/new_ui/hero1'
|
||||
import Hero2 from '../components/new_ui/hero2'
|
||||
import Wraning from '@/components/new_ui/wraning'
|
||||
// import Wraning from '@/components/new_ui/wraning'
|
||||
import Info from '@/components/new_ui/info'
|
||||
import Advantages from '@/components/new_ui/advantages'
|
||||
import OilInfo from '@/components/new_ui/oilInfo'
|
||||
import Truth from '@/components/new_ui/truth'
|
||||
import Header from '@/components/header'
|
||||
|
||||
|
||||
|
||||
|
||||
function MainPage() {
|
||||
const { location } = useRouterState()
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== '/40plus') {
|
||||
return
|
||||
}
|
||||
|
||||
let animationFrame = 0
|
||||
let retryTimeout: number | undefined
|
||||
|
||||
// Replay pending section scroll requests written to sessionStorage
|
||||
const clearTimers = () => {
|
||||
if (animationFrame) {
|
||||
window.cancelAnimationFrame(animationFrame)
|
||||
animationFrame = 0
|
||||
}
|
||||
if (retryTimeout) {
|
||||
window.clearTimeout(retryTimeout)
|
||||
retryTimeout = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const attemptScroll = (targetId: string) => {
|
||||
clearTimers()
|
||||
|
||||
const scrollToTarget = () => {
|
||||
const element = document.getElementById(targetId)
|
||||
if (element) {
|
||||
window.sessionStorage.removeItem('pendingScrollSection')
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
animationFrame = 0
|
||||
retryTimeout = undefined
|
||||
return
|
||||
}
|
||||
|
||||
retryTimeout = window.setTimeout(() => {
|
||||
animationFrame = window.requestAnimationFrame(scrollToTarget)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
animationFrame = window.requestAnimationFrame(scrollToTarget)
|
||||
}
|
||||
|
||||
const runScroll = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const targetId = window.sessionStorage.getItem('pendingScrollSection')
|
||||
if (targetId) {
|
||||
attemptScroll(targetId)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePendingEvent = () => {
|
||||
runScroll()
|
||||
}
|
||||
|
||||
runScroll()
|
||||
|
||||
window.addEventListener('pending-scroll-section', handlePendingEvent)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('pending-scroll-section', handlePendingEvent)
|
||||
clearTimers()
|
||||
}
|
||||
}, [location.pathname])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header showMenu={true} />
|
||||
<Hero1 />
|
||||
<Hero2 />
|
||||
<Wraning />
|
||||
{/* <Wraning /> */}
|
||||
<Info />
|
||||
<Advantages />
|
||||
<OilInfo />
|
||||
<Truth />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
235
src/pages/recipe.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Box, Text, SimpleGrid, Image, HStack } from '@chakra-ui/react'
|
||||
import { InstagramEmbed } from 'react-social-media-embed';
|
||||
import Header from '@/components/header';
|
||||
import { useEffect, useState } from 'react'
|
||||
import { colors } from '../colors';
|
||||
import { IoCaretBackOutline } from "react-icons/io5";
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
const igPosts = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Sam Sam Kitchen",
|
||||
subTitle: "蜂蜜芥末煎三文魚",
|
||||
url: "https://www.instagram.com/p/DLrhFq6JnKV/?utm_source=ig_web_button_share_sheet"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "阿婆廚房",
|
||||
subTitle: "懶人版蝦多士",
|
||||
url: "https://www.instagram.com/reel/DMaP9jaSeDV/?utm_source=ig_web_button_share_sheet"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Cook1cook",
|
||||
subTitle: "慳油炸雞法",
|
||||
url: "https://www.instagram.com/reel/DL4i5TXtI4B/?igsh=MXBsYXoyOGJucXE0ZQ"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Travelogue by Tracy",
|
||||
subTitle: "酥炸牛油果天婦羅",
|
||||
url: "https://www.instagram.com/reel/DOYeOUfEUoP/"
|
||||
},
|
||||
|
||||
{
|
||||
id: 5,
|
||||
title: "獅球嘜",
|
||||
subTitle: "網絡爆紅油漬蒜頭",
|
||||
url: "https://www.instagram.com/p/DQlggHUD6Y7/"
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
function Recipe() {
|
||||
const router = useRouter();
|
||||
const [headerHeight, setHeaderHeight] = useState(80);
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeaderHeight = () => {
|
||||
const header = document.querySelector('[data-header="true"]');
|
||||
if (header) {
|
||||
const height = header.getBoundingClientRect().height;
|
||||
setHeaderHeight(height);
|
||||
}
|
||||
};
|
||||
|
||||
const delayedUpdate = () => {
|
||||
setTimeout(updateHeaderHeight, 100);
|
||||
};
|
||||
|
||||
delayedUpdate();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setTimeout(updateHeaderHeight, 50);
|
||||
});
|
||||
|
||||
const header = document.querySelector('[data-header="true"]');
|
||||
if (header) {
|
||||
resizeObserver.observe(header);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', delayedUpdate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', delayedUpdate);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
let retryTimeout: number | undefined;
|
||||
const scriptId = 'instagram-embed-script';
|
||||
const maxRetries = 10;
|
||||
let retries = 0;
|
||||
|
||||
const clearRetry = () => {
|
||||
if (retryTimeout) {
|
||||
window.clearTimeout(retryTimeout);
|
||||
retryTimeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleProcess = () => {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const process = window.instgrm?.Embeds?.process;
|
||||
if (process) {
|
||||
process();
|
||||
clearRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (retries < maxRetries) {
|
||||
retries += 1;
|
||||
retryTimeout = window.setTimeout(scheduleProcess, 400);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureScript = () => new Promise<void>((resolve) => {
|
||||
const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null;
|
||||
|
||||
if (existingScript) {
|
||||
if (window.instgrm?.Embeds?.process) {
|
||||
resolve();
|
||||
} else {
|
||||
existingScript.addEventListener('load', () => resolve(), { once: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.id = scriptId;
|
||||
script.src = 'https://www.instagram.com/embed.js';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => resolve();
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
ensureScript().then(() => scheduleProcess());
|
||||
scheduleProcess();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
clearRetry();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
w="100%"
|
||||
bgColor="#FFFBD3"
|
||||
zIndex={3}
|
||||
overflow="hidden"
|
||||
pt={`${headerHeight}px`}
|
||||
|
||||
>
|
||||
<Header showMenu={true} />
|
||||
|
||||
<HStack
|
||||
position="absolute"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
top={{ base: "140px", md: "140px", lg: "140px", xl: "140px" }}
|
||||
left={{ base: "0vw", md: "2vw", lg: "2vw", xl: "2vw" }}
|
||||
onClick={() => router.navigate({ to: '/40plus' })}
|
||||
cursor="pointer"
|
||||
_hover={{ opacity: 0.8 }}
|
||||
>
|
||||
<IoCaretBackOutline color={colors.topBarColor} size={50} />
|
||||
<Text
|
||||
className='font-melle font-xbold'
|
||||
color={'#468A06'}
|
||||
fontSize={{ base: "2xl", md: "2xl", lg: "2xl", xl: "2xl" }}
|
||||
>
|
||||
{'返回上一頁'}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Image
|
||||
src="/images/new/recipe_title.webp"
|
||||
w={{ base: '50vw', md: '50vw', lg: '30vw' }}
|
||||
mt={20}
|
||||
mx="auto"
|
||||
/>
|
||||
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, sm: 1, md: 2, lg: 2, xl: 3 }}
|
||||
p={{ base: 10, sm: 20, md: 20 }}
|
||||
w={{ base: '100%', sm: '100%', md: '100%', lg: '90%', xl: '90%' }}
|
||||
h={'auto'}
|
||||
gap={{ base: 10, md: 5 }}
|
||||
mx="auto"
|
||||
justifyItems={'center'}
|
||||
alignItems={'flex-start'}
|
||||
>
|
||||
{igPosts.map((post) => (
|
||||
|
||||
|
||||
<Box
|
||||
key={post.id}
|
||||
justifyItems={'flex-start'}
|
||||
alignItems={'center'}
|
||||
|
||||
>
|
||||
<Text
|
||||
color={'#3F8C04'}
|
||||
|
||||
className='font-noto-sans'
|
||||
fontWeight={'400'}
|
||||
fontSize={{ base: "2xl", md: "2xl", lg: "2xl", xl: "2xl" }}>
|
||||
{post.title}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
color={'#3F8C04'}
|
||||
mb={2}
|
||||
className='font-melle font-xbold'
|
||||
fontSize={{ base: "2xl", md: "2xl", lg: "2xl", xl: "2xl" }}>
|
||||
{post.subTitle}
|
||||
</Text>
|
||||
<InstagramEmbed url={post.url}
|
||||
width={328}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
|
||||
</SimpleGrid>
|
||||
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Recipe;
|
||||
@@ -2,29 +2,38 @@ import { createRouter, createRootRoute, createRoute } from '@tanstack/react-rout
|
||||
import Layout from './components/layout'
|
||||
import MainPage from './pages/main'
|
||||
import HomePage from './pages/home'
|
||||
import RecipePage from './pages/recipe'
|
||||
|
||||
// Create a root route with layout
|
||||
const rootRoute = createRootRoute({
|
||||
component: Layout,
|
||||
})
|
||||
|
||||
// Main page route (new landing page)
|
||||
// Home page route (main landing page at /)
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/',
|
||||
component: MainPage,
|
||||
})
|
||||
|
||||
// Home page route (old content)
|
||||
const homeRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/home',
|
||||
component: HomePage,
|
||||
})
|
||||
|
||||
// Create the router
|
||||
// Main page route (40+ page)
|
||||
const mainRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/40plus',
|
||||
component: MainPage,
|
||||
})
|
||||
|
||||
// Recipe page route (under 40+)
|
||||
const recipeRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/40plus/recipe',
|
||||
component: RecipePage,
|
||||
})
|
||||
|
||||
// Create the router with hash-based routing for FTP server compatibility
|
||||
const router = createRouter({
|
||||
routeTree: rootRoute.addChildren([indexRoute, homeRoute]),
|
||||
routeTree: rootRoute.addChildren([indexRoute, mainRoute, recipeRoute]),
|
||||
defaultPreload: 'intent',
|
||||
})
|
||||
|
||||
export default router
|
||||
5
src/types/global.d.ts
vendored
@@ -4,4 +4,9 @@ interface Window {
|
||||
parse: (element?: Element) => void;
|
||||
};
|
||||
};
|
||||
instgrm?: {
|
||||
Embeds?: {
|
||||
process?: () => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
27
traefik-docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: '3'
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.1
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--api.dashboard=true"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8080:8080" # Traefik dashboard
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- traefik-public
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||