Compare commits

...

53 Commits

Author SHA1 Message Date
e6a2bdad0b final updated 2025-11-11 21:31:07 +08:00
1a77719dfa changed fb image 2025-11-11 14:34:36 +08:00
aa4dde3d19 changed the path from / to /40+ 2025-11-11 11:24:15 +08:00
9f68f445b9 changed the page path, added old footer, fixed oil cube display ** problem 2025-11-11 11:04:50 +08:00
e8e820f710 web link updated 2025-11-10 18:45:17 +08:00
6b3d7ae3f6 changed hktv mall link and ajust font size 2025-11-10 17:44:28 +08:00
3d3974ef6d set the text size 2025-11-10 17:15:32 +08:00
18735a884d updated ui 2025-11-10 17:01:32 +08:00
6cb762bf76 unread problem fixed 2025-11-10 11:23:17 +08:00
3530f6d52e menu item scroll problem in mobile fixed 2025-11-10 11:21:44 +08:00
671b0de875 ui updated 2025-11-10 11:06:45 +08:00
2113724aaa text size and hero2 heart image size updated 2025-11-09 22:17:21 +08:00
ec349edd42 recipe updated 2025-11-09 22:00:29 +08:00
7534df0f83 updated 2025-11-09 21:37:08 +08:00
e7eccb30e0 ui updated 2025-11-09 20:47:42 +08:00
d8448f70d4 ui udated 2025-11-09 13:19:59 +08:00
a247df6d11 updated 2025-11-08 14:01:00 +08:00
82d434017d image updated 2025-11-04 13:37:43 +08:00
b09707eb95 truth component links 2025-11-04 13:32:19 +08:00
8dd83eed4f button center updated 2025-11-04 13:17:57 +08:00
8b60b0c9b6 ans box link to url 2025-11-04 13:11:17 +08:00
fcacafa237 updated supermaket images not center problem 2025-11-04 13:02:45 +08:00
96066a64d5 fixed never read 2025-11-04 12:53:43 +08:00
f5392cf597 updated ui 2025-11-04 12:50:27 +08:00
21cc9905b7 updated 2025-11-04 09:43:03 +08:00
6c7384e8b4 updated 2025-11-03 09:58:42 +08:00
8e7e69e7ce docker updated 2025-11-03 09:55:21 +08:00
56927ac9f3 docker file changed 2025-11-03 09:52:21 +08:00
708ce80dbb edited nginx.conf 2025-11-03 09:38:30 +08:00
f16f0fdb32 add nginx 2025-11-03 09:30:59 +08:00
c63f36fb59 updated docker file 2025-11-03 09:29:55 +08:00
11c699a5a6 updated docker 2025-11-03 09:26:53 +08:00
a8a918dabc remove show menu prop and related interface 2025-11-03 09:11:26 +08:00
46f5f424bc docker updated 2025-11-03 09:05:53 +08:00
c9931d670e other bowrser center problem fixed 2025-11-02 21:47:32 +08:00
144a362a16 fix recipe ig post can't load problem 2025-11-02 21:01:52 +08:00
2f4e90f0c4 header direct updated 2025-11-02 20:43:59 +08:00
999ee7224f recipe page updated 2025-11-02 20:36:37 +08:00
736578dae3 header updated 2025-11-02 19:32:26 +08:00
3c39db54d2 header scroll down hide 2025-11-02 19:16:32 +08:00
1403217253 menu to info 2025-11-02 19:05:37 +08:00
74027dc6db header updated 2025-11-02 19:00:10 +08:00
0ef384d5d2 header height adjust 2025-11-02 18:43:04 +08:00
eb6ed870a7 header updated 2025-11-02 17:44:21 +08:00
b68899b91a header updated, added menu button and drawer 2025-11-02 17:01:07 +08:00
0718271246 updated the footer 2025-11-02 16:36:30 +08:00
b190bf3357 truth component added animation 2025-11-02 16:28:57 +08:00
5d7814f983 truth component updated 2025-11-02 16:25:38 +08:00
6392ee020f oilInfo component added animation 2025-11-02 15:34:48 +08:00
63ba11891f added oilInfo component 2025-11-02 15:24:52 +08:00
c4181b48b1 advantages added animation 2025-11-02 15:00:14 +08:00
3cdcb28829 ui updated 2025-11-02 14:24:59 +08:00
9d7a9b8d94 ui updated 2025-11-02 14:18:01 +08:00
48 changed files with 2034 additions and 210 deletions

View File

View File

@@ -1,26 +1,22 @@
# Use an official Node.js runtime as a parent image FROM node:20 AS build-stage
FROM node:18-alpine
# Set the working directory
WORKDIR /app WORKDIR /app
# Copy package.json and package-lock.json COPY package*.json /app/
COPY package*.json ./
# Install dependencies
RUN npm install RUN npm install
# Copy the rest of the application code COPY ./ /app/
COPY . .
ARG VITE_API_URL=${VITE_API_URL}
# Build the application
RUN npm run build RUN npm run build
# Install a lightweight web server
RUN npm install -g serve
# Set the command to run the web server # Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
CMD ["serve", "-s", "dist"] FROM nginx:1
# Expose the port the app runs on COPY --from=build-stage /app/dist/ /usr/share/nginx/html
EXPOSE 3000
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf

View File

@@ -4,15 +4,28 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: healthy-oil container_name: healthy-oil
ports: ports:
- "3010:3000" - "3015:80"
volumes:
- .:/app
- /app/node_modules
networks: 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: networks:
gitea_network: traefik-public:
external: true

11
nginx copy.conf Normal file
View 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;
}

View File

@@ -0,0 +1,9 @@
location /api {
return 404;
}
location /docs {
return 404;
}
location /redoc {
return 404;
}

View File

@@ -1,5 +1,5 @@
server { server {
listen 3010; listen 80;
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;

129
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"react-facebook": "^9.0.12", "react-facebook": "^9.0.12",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-lazy-load-image-component": "^1.6.3", "react-lazy-load-image-component": "^1.6.3",
"react-social-media-embed": "^2.5.18",
"react-social-plugins": "^2.1.0", "react-social-plugins": "^2.1.0",
"snippet": "^0.1.0" "snippet": "^0.1.0"
}, },
@@ -1911,6 +1912,12 @@
"@types/react": "^19.0.0" "@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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.26.1", "version": "8.26.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz",
@@ -3683,6 +3690,12 @@
"node": ">= 6" "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": { "node_modules/cli-table": {
"version": "0.3.11", "version": "0.3.11",
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz",
@@ -4201,7 +4214,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
@@ -4975,6 +4987,12 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT" "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": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -5695,6 +5713,16 @@
"react": ">= 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": { "node_modules/react-icons": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@@ -5733,6 +5761,40 @@
"node": ">=0.10.0" "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": { "node_modules/react-social-plugins": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-social-plugins/-/react-social-plugins-2.1.0.tgz", "resolved": "https://registry.npmjs.org/react-social-plugins/-/react-social-plugins-2.1.0.tgz",
@@ -5757,6 +5819,33 @@
"node": ">=0.10.0" "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": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -5895,6 +5984,12 @@
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT" "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": { "node_modules/scule": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
@@ -6000,6 +6095,12 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -6804,6 +6905,32 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zod": {
"version": "3.24.2", "version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",

View File

@@ -25,6 +25,7 @@
"react-facebook": "^9.0.12", "react-facebook": "^9.0.12",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-lazy-load-image-component": "^1.6.3", "react-lazy-load-image-component": "^1.6.3",
"react-social-media-embed": "^2.5.18",
"react-social-plugins": "^2.1.0", "react-social-plugins": "^2.1.0",
"snippet": "^0.1.0" "snippet": "^0.1.0"
}, },

16
public/.htaccess Normal file
View 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]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
public/images/fb.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
public/images/new/skin.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

View File

@@ -1,11 +1,15 @@
import { Stack, Flex, Image, Text, Box } from '@chakra-ui/react'; import { Stack, Flex, Image, Text, Box } from '@chakra-ui/react';
import { colors } from '../colors'; import { colors } from '../colors';
const footerText = `*獅球嘜品牌產品系列中首次推出之護心食用油 // const footerText = `*獅球嘜品牌產品系列中首次推出之護心食用油
+根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製配方以每日25%的總攝取能量計算 // +根據世界衛生組織建議每日人體脂肪酸攝取黃金比例而調製配方以每日25%的總攝取能量計算
#每食用分量含0克反式脂肪 // #每食用分量含0克反式脂肪
▲Omega-6不飽和脂肪酸在體內代謝會產生花生四烯酸進而產生促發炎的前列腺素導致血管收縮及慢性發炎 // ▲Omega-6不飽和脂肪酸在體內代謝會產生花生四烯酸進而產生促發炎的前列腺素導致血管收縮及慢性發炎
▼營萃護心油的omega-3比例較一般花生油、粟米油高特別有助平衡外出用餐多的都市人其身體脂肪酸比例增加抗發炎維持心血管健康` // ▼營萃護心油的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() { function Footer() {
const formatText = (text: string) => { const formatText = (text: string) => {
@@ -51,7 +55,8 @@ function Footer() {
<Flex <Flex
alignItems={'flex-end'} alignItems={'flex-end'}
w={{ base: '100%', sm: '100%', md: '60%', lg: '55%', xl: '55%' }} 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' className='font-noto-sans font-regular'
color={colors.textColor} color={colors.textColor}
fontSize={'10px'} fontSize={'10px'}
mt={2} // mt={2}
ml={2} ml={2}
> >
{`Copyright © ${currentYear}合興食油(香港)有限公司 版權所有,不得轉載。`} {`Copyright © ${currentYear}合興食油(香港)有限公司 版權所有,不得轉載。`}

View File

@@ -1,13 +1,262 @@
import { Box, Flex, Image, } from '@chakra-ui/react'; import { Box, Flex, Image, VStack, Text, Link } from '@chakra-ui/react';
import {colors} from '../colors'; 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 ( return (
<Box bg={colors.topBarColor} py={4} w="full"> <>
<Flex alignItems={'center'} justifyContent={'center'} direction="column"> <Box
<Image src="/images/header_logo.webp" alt="Logo" width="150px" /> ref={headerRef}
</Flex> bg={colors.topBarColor}
</Box> 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>
</>
); );
} }

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

View File

@@ -1,20 +1,28 @@
import { Outlet } from '@tanstack/react-router' import { Outlet, useRouterState } from '@tanstack/react-router'
import { import {
Box, Box,
Container, Container,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import Header from './header'
import Footer from './footer' import Footer from './footer'
import OldFooter from './oldFooter'
function Layout() { function Layout() {
const { location } = useRouterState()
// Use oldFooter for main page (/), footer for /40+ pages
const isMainPage = location.pathname === '/'
return ( return (
<Container maxW="100vw" p={0} m={0} mt={0}> <Container maxW="100vw" p={0} m={0} mt={0}>
<Header /> {/* <Header showMenu={showMenu} /> */}
<Box position="relative"> <Box position="relative">
<Outlet /> <Outlet />
</Box> </Box>
<Footer /> {isMainPage ? <OldFooter /> : <Footer />}
</Container> </Container>
) )
} }

View File

@@ -12,6 +12,7 @@ interface CyclingImageProps {
right?: any right?: any
bottom?: any bottom?: any
display?: any display?: any
transform?: any
cycleDuration?: number // Duration of one complete cycle in seconds cycleDuration?: number // Duration of one complete cycle in seconds
intensity?: number // How dark/light it gets (0-1, where 1 is maximum) intensity?: number // How dark/light it gets (0-1, where 1 is maximum)
timingFunction?: string // CSS timing function timingFunction?: string // CSS timing function
@@ -31,6 +32,7 @@ const CyclingImage = ({
right, right,
bottom, bottom,
display, display,
transform,
cycleDuration = 3, cycleDuration = 3,
intensity = 0.5, intensity = 0.5,
timingFunction = 'ease-in-out', timingFunction = 'ease-in-out',
@@ -95,6 +97,7 @@ const CyclingImage = ({
w={w} w={w}
maxW={maxW} maxW={maxW}
left={left} left={left}
transform={transform}
top={top} top={top}
right={right} right={right}
bottom={bottom} bottom={bottom}

View File

@@ -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 { motion, useInView } from 'framer-motion'
import { useRef } from 'react' import { useRef } from 'react'
const MotionImage = motion.create(Image) const MotionImage = motion.create(Image)
const MotionFlex = motion.create(Flex) const MotionFlex = motion.create(Flex)
const MotionBox = motion.create(Box) const MotionBox = motion.create(Box)
import { colors } from '../../colors';
const MotionStack = motion(Stack); const MotionStack = motion(Stack);
interface InfoData { const MotionHStack = motion(HStack);
id: number const MotionSimpleGrid = motion.create(SimpleGrid);
title: string const MotionText = motion(Text);
image: string import { colors } from '../../colors';
titleImage: string
titleImage2: string function contextPeopleImageMt(index: number) {
description: string
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 = [ const oilCube = [
{ {
@@ -49,19 +89,20 @@ const info = [
{ {
id: 1, id: 1,
title: "減少壞膽固醇", title: "減少壞膽固醇",
description: `**根據世界衛生組織數據高反式脂肪攝取可令全因死亡率上升34%**冠心病發病率上升 21%及死亡率上升24%。全球每年因反式脂肪導致心血管疾病死亡病例已高達54萬人¹。 description: `**根據世界衛生組織數據高反式脂肪攝取可令全因死亡率上升34%**冠心病發病率上升 21%及死亡率上升24%。全球每年因反式脂肪導致心血管疾病死亡病例已高達54萬人¹。
**獅球嘜營萃護心油每食用份量含0克反式脂肪**有助減少壞膽固醇,減低患心臟病嘅風險。`, **獅球嘜營萃護心油每食用份量含0克反式脂肪**有助減少壞膽固醇,減低患心臟病嘅風險。`,
image: "/images/new/1.webp", image: "/images/new/1.webp",
titleImage: "/images/new/adv_img1.webp", titleImage: "/images/new/adv_img1.webp",
titleImage2: "/images/new/adv1.webp" titleImage2: "/images/new/adv1.webp"
}, },
{ {
id: 2, id: 2,
title: "黃金比例脂肪酸調配", title: `黃金比例
description: `根據世衛建議,脂肪攝取應以**不飽和脂肪為主,**當中包括多元與單元不飽和脂肪酸,同時應減少飽和脂肪攝取,以維持整體健康比例。 脂肪酸調配`,
description: `根據世衛建議,脂肪攝取應以**不飽和脂肪為主**,當中包括單元與多元不飽和脂肪酸,同時應減少飽和脂肪攝取,以維持整體健康比例。
獅球嘜營萃護心油就係根據呢個原則調配出符合**世衛黃金比例**嘅配方²,幫助身體攝取每日必需脂肪酸,並促進**脂溶性維他命A、D、E、K**嘅吸收,全面支援日常健康所需。 `, 獅球嘜營萃護心油就係根據呢個原則調配出符合**世衛黃金比例**嘅配方²,幫助身體攝取每日必需脂肪酸,並促進**脂溶性維他命A、D、E、K**嘅吸收,全面支援日常健康所需。 `,
image: "/images/new/2.webp", image: "/images/new/2.webp",
titleImage: "/images/new/adv_img2.webp", titleImage: "/images/new/adv_img2.webp",
titleImage2: "/images/new/adv2.webp" titleImage2: "/images/new/adv2.webp"
@@ -69,9 +110,9 @@ const info = [
{ {
id: 3, id: 3,
title: "比一般食油", title: "比一般食油",
description: `**Omega-3有助降低三酸甘油脂水平**並喺一定程度上減少血管發炎反應,從而支援血管彈性及正常血液循環,為心臟健康打好基礎。 description: `**Omega-3有助降低三酸甘油脂水平**並喺一定程度上減少血管發炎反應,從而支援血管彈性及正常血液循環,為心臟健康打好基礎。
**獅球嘜營萃護心油內嘅Omega-3比例較一般花生油及粟米油高**特別有助經常外出用餐嘅都市人平衡脂肪酸攝取比例,從而支援心血管健康。 `, **獅球嘜營萃護心油內嘅Omega-3比例較一般花生油及粟米油高**特別有助經常外出用餐嘅都市人平衡脂肪酸攝取比例,從而支援心血管健康。 `,
image: "/images/new/3.webp", image: "/images/new/3.webp",
titleImage: "/images/new/adv_img3.webp", titleImage: "/images/new/adv_img3.webp",
titleImage2: "/images/new/adv3.webp" titleImage2: "/images/new/adv3.webp"
@@ -124,84 +165,331 @@ function Advantages() {
const oilCubesRef = useRef(null); const oilCubesRef = useRef(null);
const isOilCubesInView = useInView(oilCubesRef, { once: true }); const isOilCubesInView = useInView(oilCubesRef, { once: true });
const mainRef = useRef(null);
const isMainInView = useInView(mainRef, { once: true });
return ( return (
<Box <MotionBox
id="advantages"
ref={mainRef}
position="relative" position="relative"
w="100%" w="100%"
h="auto" h="auto"
bgColor="#9AC035" bgColor="#9AC035"
zIndex={3} zIndex={3}
overflow="hidden"
justifyItems={'center'} justifyItems={'center'}
overflow="hidden"
initial={{ opacity: 0 }}
animate={isMainInView ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
> >
<SimpleGrid <Stack
columns={{ base: 1, sm: 1, md: 2 }} position="relative"
w="100%"
justifyItems={'center'}
alignItems={'center'}
gap={0}
>
<Image
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} p={4}
w={{ base: '50vw', sm: '90vw', md: '90vw', lg: '80vw', xl: '70vw' }} pb={10}
mt={20}
w={{ base: '100%', sm: '95vw', md: '90vw', lg: '85vw', xl: '65vw' }}
h={'auto'} 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) => ( {info.map((item, index) => (
<Stack <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%'} w={'100%'}
alignItems="center" alignItems="center"
key={item.id}> key={item.id}
<Image initial={{ opacity: 0, y: 30 }}
w={{ base: '40vw', sm: '180px', md: '80px', lg: '110px', xl: '110px' }} 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} 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" }}
/> />
<Image {item.id === 3 ? (
mt={{ base: '-30px', sm: '-70px', md: '-30px', lg: '-50px', xl: '-50px' }} <>
h={{ base: '40vw', sm: '80px', md: '60px', lg: '80px', xl: '80px' }} <MotionText
src={item.titleImage2} 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" }}
/>
<Text <MotionText
mt={{ base: '-25px', sm: '-25px', md: '-15px', lg: '-25px', xl: '-25px' }} mt={{ base: '-25px', sm: '-25px', md: '-15px', lg: '-25px', xl: '-25px' }}
className='font-melle font-xbold' className='font-melle font-xbold'
fontWeight={400} fontWeight={400}
color='white' 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 }}
{item.title} animate={isMainInView ? { opacity: 1, x: 0 } : { opacity: 0, x: -20 }}
</Text> transition={{ duration: 0.5, delay: index * 0.15 + 0.3, ease: "easeOut" }}
<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' }} {item.title}
</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} 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 <MotionText
w={{ base: '90%', sm: '70%', md: '80%', lg: '80%', xl: '80%' }} w={{ base: '90%', sm: '80%', md: '80%', lg: '80%', xl: '80%' }}
className="font-noto-sans font-demi-light" 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" color="white"
lineHeight="1.8" lineHeight="1.8"
textAlign="left" textAlign="left"
whiteSpace="pre-wrap" 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)} {renderDescription(item.description)}
</Text> </MotionText>
{item.id === 4 ? ( {item.id === 4 ? (
<Box <Box
width="100%" width="100%"
display="flex" display="flex"
flexDirection={'column'} flexDirection={'column'}
alignItems="center" alignItems="flex-start"
ref={oilCubesRef} ref={oilCubesRef}
mt={-4} mt={-4}
ml={3} 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) => ( {oilCube.map((item, index) => (
<MotionStack <MotionStack
key={index} key={index}
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={isOilCubesInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }} animate={isOilCubesInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
transition={{ duration: 0.5, delay: index * 0.15 }} transition={{ duration: 0.5, delay: index * 0.15 }}
w='300px' w='calc(50% - 5px)'
minW='280px'
h='100px' h='100px'
bgImage={item.bgColor} bgImage={item.bgColor}
roundedTopLeft={'30px'} roundedTopLeft={'30px'}
@@ -226,20 +514,20 @@ function Advantages() {
</Text> </Text>
</MotionStack> </MotionStack>
))} ))}
</Flex> </MotionFlex>
</Box> </Box>
) : ( ) : (
<></> <></>
)} )}
</Stack> </MotionStack>
))} ))}
</SimpleGrid> </MotionSimpleGrid>
</Box > </MotionBox >
) )
} }

View File

@@ -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 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() { function Hero1() {
const [headerHeight, setHeaderHeight] = useState(80);
const bigWarningSize = { base: "17vw", sm: "15vw", md: "9vw", lg: "9vw", xl: "8vw" }; 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 ( return (
<Box <Box
position="relative" position="relative"
w="100%" w="100%"
alignItems={"center"} alignItems={"center"}
justifyContent={"center"} justifyContent={"center"}
pt={`${headerHeight}px`}
> >
{/* Global keyframes for all animations */} {/* Global keyframes for all animations */}
<style> <style>
@@ -54,60 +92,188 @@ function Hero1() {
} }
`} `}
</style> </style>
<Stack gap={0}> <Stack gap={0}>
<Box <Box
position="relative" position="relative"
w="100%" w="100%"
minH={{ base: "110vw", sm: "110vw", md: "70vw", lg: "70vw", xl: "45vw" }} minH={{ base: "220vw", sm: "220vw", md: "93vw", lg: "95vw", xl: "93vw" }}
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')"}} 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" bgSize="cover"
backgroundPosition="center" backgroundPosition="center"
justifyItems={'center'} justifyItems={'center'}
bgRepeat="no-repeat" bgRepeat="no-repeat"
> >
{/* Hero 1 title */} {/* Hero 1 title */}
<Image src="/images/new/hero_text1x.webp" <MotionImage
src="/images/new/hero_text1x.webp"
position={'absolute'} position={'absolute'}
bottom={{ base: '70px', sm: '100px', md: '100px', lg: '100px', xl: '100px' }} left="50%"
maxW={{ base: "70vw", sm: "70vw", md: "50vw", lg: "50vw", xl: "35vw" }} 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' alt='hero title'
css={{ initial={{ opacity: 0, y: 30, x: '-50%' }}
animation: 'fadeInUp 1s ease-out', whileInView={{ opacity: 1, y: 0, x: '-50%' }}
'@keyframes fadeInUp': { viewport={{ once: true, amount: 0.3 }}
from: { transition={{ duration: 0.8, ease: "easeOut" }}
opacity: 0,
transform: 'translateY(30px)'
},
to: {
opacity: 1,
transform: 'translateY(0)'
}
}
}}
/> />
{/* Mobile arrow - shown on base and sm */} {/* Mobile arrow - shown on base and sm */}
<CyclingImage <CyclingImage
src="/images/new/arrow_mobile_1.webp" src="/images/new/arrow_mobile_1.webp"
src2="/images/new/arrow_mobile_2.webp" src2="/images/new/arrow_mobile_2.webp"
position={'absolute'} 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'} bottom={'10px'}
display={{ base: "block", sm: "block", md: "none", lg: "none", xl: "none" }} display={{ base: "block", sm: "block", md: "none", lg: "none", xl: "none" }}
cycleDuration={0.5} cycleDuration={0.5}
intensity={0} intensity={0}
/> />
{/* Desktop arrow - shown on md, lg, xl */} {/* Desktop arrow - shown on md, lg, xl */}
<CyclingImage <CyclingImage
src="/images/new/arrow_pc_1.webp" src="/images/new/arrow_pc_1.webp"
src2="/images/new/arrow_pc_2.webp" src2="/images/new/arrow_pc_2.webp"
position={'absolute'} position={'absolute'}
left="50%"
transform="translateX(-50%)"
maxW={{ base: "70%", sm: "70%", md: "70%", lg: "70%", xl: "70%" }} maxW={{ base: "70%", sm: "70%", md: "70%", lg: "70%", xl: "70%" }}
bottom={'10px'} bottom={'10px'}
display={{ base: "none", sm: "none", md: "block", lg: "block", xl: "block" }} display={{ base: "none", sm: "none", md: "block", lg: "block", xl: "block" }}
cycleDuration={0.5} cycleDuration={0.5}
intensity={0} 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 */} {/* signs */}
<Box <Box
position={'absolute'} position={'absolute'}
@@ -119,7 +285,7 @@ function Hero1() {
animationDelay: '0s' animationDelay: '0s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/bigwarning.webp" src="/images/new/bigwarning.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -130,32 +296,14 @@ function Hero1() {
<Box <Box
position={'absolute'} position={'absolute'}
w={{ base: "13vw", sm: "13vw", md: "6vw", lg: "6vw", xl: "5vw" }} w={{ base: "13vw", sm: "13vw", md: "6vw", lg: "6vw", xl: "5vw" }}
left={{ base: '6vw', sm: '6vw', md: '13vw', lg: '13vw', xl: '25vw' }} left={{ base: '10vw', sm: '10vw', md: '23vw', lg: '23vw', xl: '25vw' }}
bottom={{ base: '50vw', sm: '50vw', md: '27vw', lg: '27vw', xl: '18vw' }} top={{ base: '50vw', sm: '50vw', md: '27vw', lg: '27vw', xl: '22vw' }}
css={{ css={{
animation: 'floatMedium 3.5s ease-in-out infinite', animation: 'floatMedium 3.5s ease-in-out infinite',
animationDelay: '0.5s' animationDelay: '0.5s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/bigwarning.webp"
position={'relative'}
w="100%"
cycleDuration={3}
intensity={0.1}
/>
</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' }}
css={{
animation: 'floatFast 3.2s ease-in-out infinite',
animationDelay: '1s'
}}
>
<CyclingImage
src="/images/new/bigwarning.webp" src="/images/new/bigwarning.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -166,14 +314,32 @@ function Hero1() {
<Box <Box
position={'absolute'} position={'absolute'}
w={bigWarningSize} w={bigWarningSize}
right={{ base: '11vw', sm: '11vw', md: '18vw', lg: '18vw', xl: '18vw' }} right={{ base: '7vw', sm: '7vw', md: '17vw', lg: '17vw', xl: '20vw' }}
top={{ base: '12vw', sm: '12vw', md: '20vw', lg: '20vw', xl: '10vw' }} top={{ base: '60vw', sm: '60vw', md: '30vw', lg: '30vw', xl: '30vw' }}
css={{
animation: 'floatFast 3.2s ease-in-out infinite',
animationDelay: '1s'
}}
>
<CyclingImage
src="/images/new/bigwarning.webp"
position={'relative'}
w="100%"
cycleDuration={3}
intensity={0.1}
/>
</Box>
<Box
position={'absolute'}
w={bigWarningSize}
right={{ base: '5vw', sm: '5vw', md: '18vw', lg: '18vw', xl: '18vw' }}
top={{ base: '12vw', sm: '12vw', md: '15vw', lg: '15vw', xl: '10vw' }}
css={{ css={{
animation: 'floatSlow 3.3s ease-in-out infinite', animation: 'floatSlow 3.3s ease-in-out infinite',
animationDelay: '1.5s' animationDelay: '1.5s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/bigwarning.webp" src="/images/new/bigwarning.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -184,15 +350,15 @@ function Hero1() {
{/* Warning Texts */} {/* Warning Texts */}
<Box <Box
position={'absolute'} position={'absolute'}
w={{ base: "20vw", sm: "20vw", md: "11vw", lg: "11vw", xl: "10vw" }} w={{ base: "25vw", sm: "25vw", md: "11vw", lg: "11vw", xl: "10vw" }}
left={{ base: "23vw", sm: "23vw", md: "30vw", lg: "30vw", xl: "37vw" }} left={{ base: "23vw", sm: "23vw", md: "37vw", lg: "37vw", xl: "37vw" }}
top={{ base: "9vw", sm: "9vw", md: "7vw", lg: "7vw", xl: "3vw" }} top={{ base: "9vw", sm: "9vw", md: "4vw", lg: "4vw", xl: "3vw" }}
css={{ css={{
animation: 'floatMedium 3.2s ease-in-out infinite', animation: 'floatMedium 3.2s ease-in-out infinite',
animationDelay: '0.2s' animationDelay: '0.2s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/threehightext.webp" src="/images/new/threehightext.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -202,15 +368,15 @@ function Hero1() {
</Box> </Box>
<Box <Box
position={'absolute'} position={'absolute'}
w={{ base: "20vw", sm: "20vw", md: "10vw", lg: "10vw", xl: "9vw" }} w={{ base: "27vw", sm: "27vw", md: "10vw", lg: "10vw", xl: "9vw" }}
left={{ base: "35vw", sm: "35vw", md: "18vw", lg: "18vw", xl: "25vw" }} left={{ base: "35vw", sm: "35vw", md: "23vw", lg: "23vw", xl: "25vw" }}
top={{ base: "25vw", sm: "25vw", md: "20vw", lg: "20vw", xl: "14vw" }} top={{ base: "25vw", sm: "25vw", md: "14vw", lg: "14vw", xl: "14vw" }}
css={{ css={{
animation: 'floatFast 3.5s ease-in-out infinite', animation: 'floatFast 3.5s ease-in-out infinite',
animationDelay: '0.7s' animationDelay: '0.7s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/fattext.webp" src="/images/new/fattext.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -220,15 +386,15 @@ function Hero1() {
</Box> </Box>
<Box <Box
position={'absolute'} position={'absolute'}
w={{ base: "29vw", sm: "29vw", md: "14vw", lg: "14vw", xl: "13vw" }} w={{ base: "34vw", sm: "34vw", md: "14vw", lg: "14vw", xl: "13vw" }}
right={{ base: "20vw", sm: "20vw", md: "34vw", lg: "34vw", xl: "35vw" }} right={{ base: "15vw", sm: "15vw", md: "34vw", lg: "34vw", xl: "35vw" }}
top={{ base: "10vw", sm: "10vw", md: "7vw", lg: "7vw", xl: "5vw" }} top={{ base: "10vw", sm: "10vw", md: "7vw", lg: "7vw", xl: "5vw" }}
css={{ css={{
animation: 'floatSlow 3.8s ease-in-out infinite', animation: 'floatSlow 3.8s ease-in-out infinite',
animationDelay: '0.4s' animationDelay: '0.4s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/centerfattext.webp" src="/images/new/centerfattext.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -238,15 +404,15 @@ function Hero1() {
</Box> </Box>
<Box <Box
position={'absolute'} position={'absolute'}
w={{ base: "13vw", sm: "13vw", md: "8vw", lg: "8vw", xl: "7vw" }} w={{ base: "19vw", sm: "19vw", md: "8vw", lg: "8vw", xl: "7vw" }}
right={{ base: "14vw", sm: "14vw", md: "26vw", lg: "26vw", xl: "26vw" }} right={{ base: "10vw", sm: "10vw", md: "28vw", lg: "28vw", xl: "26vw" }}
top={{ base: "35vw", sm: "35vw", md: "18vw", lg: "18vw", xl: "12vw" }} top={{ base: "35vw", sm: "35vw", md: "14vw", lg: "14vw", xl: "12vw" }}
css={{ css={{
animation: 'floatFast 3.3s ease-in-out infinite', animation: 'floatFast 3.3s ease-in-out infinite',
animationDelay: '0.9s' animationDelay: '0.9s'
}} }}
> >
<CyclingImage <CyclingImage
src="/images/new/hairlosstext.webp" src="/images/new/hairlosstext.webp"
position={'relative'} position={'relative'}
w="100%" w="100%"
@@ -254,6 +420,61 @@ function Hero1() {
intensity={0.1} intensity={0.1}
/> />
</Box> </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> </Box>
</Stack> </Stack>
</Box> </Box>

View File

@@ -13,10 +13,10 @@ function Hero2() {
> >
<Stack gap={0}> <Stack gap={0}>
<Box <Stack
position="relative" position="relative"
w="100%" 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')"} bgImage={"url('/images/new/hero2bg.webp')"}
bgSize="cover" bgSize="cover"
backgroundPosition="center" backgroundPosition="center"
@@ -28,10 +28,11 @@ function Hero2() {
<MotionImage <MotionImage
src="/images/new/hero2title.webp" src="/images/new/hero2title.webp"
position={'absolute'} position={'absolute'}
top={{ base: "10px", sm: "50px", md: "50px", lg: "50px", xl: "50px" }} left="50%"
w={{ base: "70vw", sm: "60vw", md: "50vw", lg: "35vw", xl: "35vw" }} top={{ base: "10px", sm: "20px", md: "30px", lg: "30px", xl: "40px" }}
initial={{ opacity: 0, y: -30 }} w={{ base: "70vw", sm: "70vw", md: "35vw", lg: "35vw", xl: "28vw" }}
whileInView={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: -30, x: '-50%' }}
whileInView={{ opacity: 1, y: 0, x: '-50%' }}
viewport={{ once: true, amount: 0.3 }} viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
/> />
@@ -39,9 +40,9 @@ function Hero2() {
src="/images/new/hero2subtitle.webp" src="/images/new/hero2subtitle.webp"
position={'absolute'} position={'absolute'}
zIndex={1} zIndex={1}
top={{ base: "30vw", sm: "35vw", md: "24vw", lg: "20vw", xl: "18vw" }} top={{ base: "35vw", sm: "35vw", md: "17vw", lg: "15vw", xl: "12vw" }}
right={{ base: "15vw", sm: "15vw", md: "20vw", lg: "28vw", xl: "30vw" }} right={{ base: "55vw", sm: "55vw", md: "24vw", lg: "25vw", xl: "30vw" }}
w={{ base: "30vw", sm: "30vw", md: "25vw", lg: "18vw", xl: "18vw" }} w={{ base: "34vw", sm: "34vw", md: "22vw", lg: "22vw", xl: "18vw" }}
initial={{ opacity: 0, x: 30 }} initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, amount: 0.3 }} viewport={{ once: true, amount: 0.3 }}
@@ -49,22 +50,23 @@ function Hero2() {
/> />
<MotionImage <MotionImage
src="/images/new/oil.webp" src="/images/new/oil_new.webp"
zIndex={0} zIndex={0}
position={'absolute'} position={'absolute'}
bottom={{ base: -10, sm: -20, md: -20, lg: -20, xl: -20 }} left="50%"
w={{ base: "80vw", sm: "70vw", md: "60vw", lg: "50vw", xl: "40vw" }} bottom={0}
initial={{ opacity: 0, y: 50 }} w={{ base: "150vw", sm: "150vw", md: "58vw", lg: "53vw", xl: "40vw" }}
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 }} viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.8, delay: 0.5 }} transition={{ duration: 0.8, delay: 0.5 }}
/> />
</Box> </Stack>
</Stack> </Stack>
</Box> </Box >
); );
} }
export default Hero2; export default Hero2;

View File

@@ -11,19 +11,20 @@ interface InfoData {
title: string title: string
image: string image: string
description: string description: string
url?: string
} }
const infoData: InfoData[] = [ const infoData: InfoData[] = [
{ {
id: 1, id: 1,
title: '膽固醇\n超標', title: '膽固醇\n超標',
image: '/images/new/info_1.webp', image: '/images/new/blood.webp',
description: `長期攝取過多**飽和脂肪及反式脂肪(例如牛油、加工食品、或重複使用嘅煎炸油**,會令體內**壞膽固醇LDL上升、好膽固醇HDL下降**,導致膽固醇積聚喺血管內壁,形成血塊堵塞血管,有機會引致**中風、心肌梗塞,**甚至**心臟病**等嚴重後果。` description: `長期攝取過多**飽和脂肪及反式脂肪(如牛油、加工食品、或重複使用嘅煎炸油)**,會令體內**壞膽固醇(LDL)上升、好膽固醇(HDL)下降**,導致膽固醇積聚喺血管內壁,形成血塊堵塞血管,有機會引致**中風、心肌梗塞,**甚至**心臟病**等嚴重後果。`,
}, },
{ {
id: 2, id: 2,
title: '中央肥胖\n脂肪肝', title: '中央肥胖\n脂肪肝',
image: '/images/new/info_2.webp', image: '/images/new/liver.webp',
description: `研究指出,將煎炸油長時間加熱至超過煙點或重複使用,會令**油質氧化,**產生聚合物及自由基。前者會令油變黏稠、有油膩味,**增加肝臟負擔;**後者會攻擊細胞,加速身體衰老並提升患上慢性病嘅風險。 description: `研究指出,將煎炸油長時間加熱至超過煙點或重複使用,會令**油質氧化,**產生聚合物及自由基。前者會令油變黏稠、有油膩味,**增加肝臟負擔;**後者會攻擊細胞,加速身體衰老並提升患上慢性病嘅風險。
長期攝取呢類油脂,會干擾身體脂肪代謝速度,**令過多脂肪積聚喺肝臟,**並削弱其解毒功能,令毒素累積喺體內,最終導致**脂肪肝**。如果肝臟持續受損,可能就會留下永久損傷,最後導致**肝硬化**。` 長期攝取呢類油脂,會干擾身體脂肪代謝速度,**令過多脂肪積聚喺肝臟,**並削弱其解毒功能,令毒素累積喺體內,最終導致**脂肪肝**。如果肝臟持續受損,可能就會留下永久損傷,最後導致**肝硬化**。`
@@ -31,7 +32,7 @@ const infoData: InfoData[] = [
{ {
id: 3, id: 3,
title: '皮膚暗啞\n容易脫髮', title: '皮膚暗啞\n容易脫髮',
image: '/images/new/info_3.webp', image: '/images/new/skin.webp',
description: `好多人以為「滴油不沾」先健康,但其實**持續攝取適量又優質嘅油脂,**先可以促進毛髮健康生長,同時令皮膚保持彈性同水潤。 description: `好多人以為「滴油不沾」先健康,但其實**持續攝取適量又優質嘅油脂,**先可以促進毛髮健康生長,同時令皮膚保持彈性同水潤。
因為**脂肪係製造荷爾蒙**嘅重要原素而脂溶性維他命A、D、E、K亦必須喺有油嘅情況下先可以被吸收長遠先能真正發揮對皮膚、頭髮同整體健康嘅作用呈現由內而外嘅自然光澤。` 因為**脂肪係製造荷爾蒙**嘅重要原素而脂溶性維他命A、D、E、K亦必須喺有油嘅情況下先可以被吸收長遠先能真正發揮對皮膚、頭髮同整體健康嘅作用呈現由內而外嘅自然光澤。`
@@ -39,10 +40,10 @@ const infoData: InfoData[] = [
{ {
id: 4, id: 4,
title: '經常容易\n覺得疲倦', title: '經常容易\n覺得疲倦',
image: '/images/new/info_4.webp', image: '/images/new/brain.webp',
description: `香港人生活節奏急促,一日三餐唔係叫外賣就出街食,而餐廳普遍使用 Omega-6 偏高嘅食油如大豆油。**長期攝取會令體內Omega-6遠多於Omega-3脂肪酸比例失衡促進身體慢性發炎。** description: `香港人生活節奏急促,一日三餐唔係叫外賣就出街食,而餐廳普遍使用 Omega-6 偏高嘅食油(如大豆油)。**長期攝取會令體內Omega-6遠多於Omega-3脂肪酸比例失衡促進身體慢性發炎。**
失衡嘅狀況如果未能得到改善,會慢慢出現易攰、記憶力下降、專注力不足等情況;若持續惡化,仲可能影響腦部神經傳導,進一步提升患上**腦霧失智症**風險。 ` 如果失衡未能改善尤其Omega-3長期攝取不足就容易出現易攰、記憶力下降、專注力不足等情況。隨住狀況持續惡化,仲可能影響腦部神經傳導,提升**腦霧失智症**風險。`
} }
] ]
@@ -66,6 +67,7 @@ function Info() {
return ( return (
<Box <Box
id="info"
position="relative" position="relative"
w="100%" w="100%"
minH={{ base: "163vw", sm: "163vw", md: "75vw", lg: "50vw", xl: "45vw" }} minH={{ base: "163vw", sm: "163vw", md: "75vw", lg: "50vw", xl: "45vw" }}
@@ -103,7 +105,7 @@ function Info() {
cursor="pointer" cursor="pointer"
onClick={() => setSelectedInfo(info.id)} onClick={() => setSelectedInfo(info.id)}
bg={selectedInfo === info.id ? '#3D6741' : '#BCBCBC'} 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' }} borderRadius={{ base: '15px', sm: '15px', md: '18px', lg: '20px', xl: '20px' }}
display="flex" display="flex"
alignItems="center" alignItems="center"
@@ -111,13 +113,13 @@ function Info() {
textAlign="center" textAlign="center"
fontFamily="'MElleHK', sans-serif" fontFamily="'MElleHK', sans-serif"
fontWeight={selectedInfo === info.id ? 800 : 500} 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'} color={selectedInfo === info.id ? '#FFFFFF' : '#DDDDDD'}
whiteSpace="pre-line" whiteSpace="pre-line"
lineHeight="1.2" lineHeight="1.2"
transition="all 0.3s ease" transition="all 0.3s ease"
px={2} 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} {info.title}
</Box> </Box>
@@ -147,7 +149,7 @@ function Info() {
pb={{ base: 5, sm: 5, md: 6, lg: 9, xl: 9 }}> pb={{ base: 5, sm: 5, md: 6, lg: 9, xl: 9 }}>
<Text <Text
className="font-noto-sans font-regular" 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" color="#3D6741"
lineHeight="1.8" lineHeight="1.8"
textAlign="left" textAlign="left"

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

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

View File

@@ -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')" }} 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" bgSize="cover"
backgroundPosition="center" backgroundPosition="center"
justifyItems={'center'}
bgRepeat="no-repeat" bgRepeat="no-repeat"
zIndex={3} zIndex={3}
overflow="hidden" overflow="hidden"
@@ -146,17 +145,19 @@ function Wraning() {
{/* title */} {/* title */}
<MotionStack <MotionStack
position="absolute" position="absolute"
left="50%"
w = {'100%'}
top={{ base: '55vw', sm: '60vw', md: '35vw', lg: '23vw', xl: '20vw' }} top={{ base: '55vw', sm: '60vw', md: '35vw', lg: '23vw', xl: '20vw' }}
alignItems={'center'} alignItems={'center'}
gap={'1px'} gap={'1px'}
initial={{ opacity: 0, y: 50 }} initial={{ opacity: 0, y: 50, x: '-50%' }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0, x: '-50%' }}
viewport={{ once: true, amount: 0.3 }} viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.8, ease: "easeOut" }} transition={{ duration: 0.8, ease: "easeOut" }}
> >
<MotionText <MotionText
className="font-melle font-xbold" 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"} color={"#FED407"}
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}

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

View File

@@ -7,15 +7,15 @@ function Salespoint() {
const shopImages = [ const shopImages = [
{ {
image: '/images/pp.webp', 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', 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', 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' }} _focus={{ outline: 'none', boxShadow: 'none' }}
_active={{ outline: 'none', boxShadow: 'none' }}> _active={{ outline: 'none', boxShadow: 'none' }}>
<Image <Image
src={'/images/facebook.webp'} src={'/images/fb.webp'}
alt="salespoint" alt="salespoint"
w={"400px"} w={"400px"}
loading='lazy' loading='lazy'

View File

@@ -6,16 +6,24 @@ interface DrawerContentProps extends ChakraDrawer.ContentProps {
portalled?: boolean portalled?: boolean
portalRef?: React.RefObject<HTMLElement> portalRef?: React.RefObject<HTMLElement>
offset?: ChakraDrawer.ContentProps["padding"] offset?: ChakraDrawer.ContentProps["padding"]
positionerProps?: React.ComponentProps<typeof ChakraDrawer.Positioner>
} }
export const DrawerContent = React.forwardRef< export const DrawerContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
DrawerContentProps DrawerContentProps
>(function DrawerContent(props, ref) { >(function DrawerContent(props, ref) {
const { children, portalled = true, portalRef, offset, ...rest } = props const {
children,
portalled = true,
portalRef,
offset,
positionerProps,
...rest
} = props
return ( return (
<Portal disabled={!portalled} container={portalRef}> <Portal disabled={!portalled} container={portalRef}>
<ChakraDrawer.Positioner padding={offset}> <ChakraDrawer.Positioner padding={offset} {...positionerProps}>
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}> <ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
{children} {children}
</ChakraDrawer.Content> </ChakraDrawer.Content>

View File

@@ -6,10 +6,11 @@ import Qa from '../components/qa'
import Oil_info from '../components/oil_info' import Oil_info from '../components/oil_info'
import Bestoil from '../components/bestoil' import Bestoil from '../components/bestoil'
import Salespoint from '../components/salespoint' import Salespoint from '../components/salespoint'
import Header2 from '@/components/header2'
function HomePage() { function HomePage() {
return ( return (
<> <>
<Header2 />
<Hero1 /> <Hero1 />
<Box mt={{ base: "-5vw", sm: "-5vw", md: "-5vw", lg: "-5vw", xl: "-5vw" }}> <Box mt={{ base: "-5vw", sm: "-5vw", md: "-5vw", lg: "-5vw", xl: "-5vw" }}>
<Hero2 /> <Hero2 />

View File

@@ -1,21 +1,97 @@
import { useEffect } from 'react'
import { useRouterState } from '@tanstack/react-router'
import Hero1 from '../components/new_ui/hero1' import Hero1 from '../components/new_ui/hero1'
import Hero2 from '../components/new_ui/hero2' 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 Info from '@/components/new_ui/info'
import Advantages from '@/components/new_ui/advantages' 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() { 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 ( return (
<> <>
<Header showMenu={true} />
<Hero1 /> <Hero1 />
<Hero2 /> <Hero2 />
<Wraning /> {/* <Wraning /> */}
<Info /> <Info />
<Advantages /> <Advantages />
<OilInfo />
<Truth />
</> </>
) )
} }

235
src/pages/recipe.tsx Normal file
View 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;

View File

@@ -2,29 +2,38 @@ import { createRouter, createRootRoute, createRoute } from '@tanstack/react-rout
import Layout from './components/layout' import Layout from './components/layout'
import MainPage from './pages/main' import MainPage from './pages/main'
import HomePage from './pages/home' import HomePage from './pages/home'
import RecipePage from './pages/recipe'
// Create a root route with layout // Create a root route with layout
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: Layout, component: Layout,
}) })
// Main page route (new landing page) // Home page route (main landing page at /)
const indexRoute = createRoute({ const indexRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: '/', path: '/',
component: MainPage,
})
// Home page route (old content)
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/home',
component: HomePage, 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({ const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, homeRoute]), routeTree: rootRoute.addChildren([indexRoute, mainRoute, recipeRoute]),
defaultPreload: 'intent',
}) })
export default router export default router

View File

@@ -4,4 +4,9 @@ interface Window {
parse: (element?: Element) => void; parse: (element?: Element) => void;
}; };
}; };
instgrm?: {
Embeds?: {
process?: () => void;
};
};
} }

View 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