first commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# src/components/site/AGENTS.md
|
||||
|
||||
## Purpose
|
||||
|
||||
前台 React 元件(Astro island 內容),用 Chakra UI v3 建立一頁式官網、Blog 列表同文章頁。
|
||||
|
||||
## Ownership
|
||||
|
||||
- 擁有 `src/components/site/` 所有 `.tsx` 元件。
|
||||
- 唔擁有資料讀取(由 `.astro` 傳 props)同 Chakra theme(屬父層 `theme/system.ts`)。
|
||||
|
||||
## Local Contracts
|
||||
|
||||
- **Chakra v3 + Astro**:一個頁面只掛一個 React root island。`Provider`(`<ChakraProvider value={system}>`)由 `SiteChrome`(首頁)或 `BlogIndex` / `BlogPost` 包住。**唔可以用 `client:only`**(會失去 SEO)。
|
||||
- **版型**:`Section`(統一 max-width / padding / 背景 tone)+ `SectionHeading` 包每個 section。
|
||||
- **Chrome**:`SiteChrome` 組合 `Header` + `<main>` + `Footer` + `WhatsAppFab`;Blog 頁自行組合同一批元件。
|
||||
- **資料**:全部經 props 傳入(型別來自 `data/content.ts` / `db/schema.ts`);元件**唔直接讀 DB**。
|
||||
- **樣式 token**:用 `theme/system.ts` 的語意 token(`brand.*`=琥珀金、`ink`=墨黑、`fg.muted`、`border.subtle`、`bg.subtle`),避免散落硬編色值(現時仍有少量硬編 hex,新增時優先跟 token)。
|
||||
- **設計風格**:editorial 極簡風(見 `docs/superpowers/specs/2026-09-11-editorial-redesign-design.md`)——暖米白底、Noto Serif HK 標題、髮絲線分隔、編號式清單。**禁止**:漸層、背景光斑、`rounded-full` 膠囊掣、pill badge 眉題、陰影浮卡、icon 圓角磚。
|
||||
- **圖示**:`lucide-react` 幼線 icon(`strokeWidth={1.5}` 為佳);內容項目**唔再用** `extra` 圖示代碼(`service`/`feature` 嘅 `extra` 已停用;`step` 嘅 `extra` 係步驟編號 `01`–`05`)。
|
||||
- **響應式**:用 Chakra 的 `base` / `md` / `lg` object syntax。
|
||||
- 文案一律繁體中文(廣東話)。
|
||||
|
||||
## Work Guidance
|
||||
|
||||
- 新增首頁 section:建立元件 → 喺 `HomePage.tsx` 接入 → 資料由 `getHomeData`(`data/content.ts`)提供。
|
||||
- 新元件唔好各自建立 `Provider`,跟返現有最外層包法。
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm run build`。
|
||||
- `npm run dev` 目測 RWD、錨點、FAQ accordion、WhatsApp link、無 console / hydration error。
|
||||
|
||||
## Child DOX Index
|
||||
|
||||
無。
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Box, Container, Grid, HStack, Heading, Image, Stack, Text } from "@chakra-ui/react";
|
||||
import { ArrowRight, CalendarDays } from "lucide-react";
|
||||
import type { Settings } from "../../data/content";
|
||||
import { Footer } from "./Footer";
|
||||
import { Header } from "./Header";
|
||||
import { Provider } from "./Provider";
|
||||
import { WhatsAppFab } from "./WhatsAppFab";
|
||||
|
||||
export type BlogListItem = {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string | null;
|
||||
coverImage: string | null;
|
||||
dateLabel: string;
|
||||
};
|
||||
|
||||
export function BlogIndex({ posts, settings }: { posts: BlogListItem[]; settings: Settings }) {
|
||||
const [featured, ...rest] = posts;
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<Header settings={settings} />
|
||||
<Box as="main" py={{ base: 14, md: 20 }} minH="60vh">
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
<Stack gap="5" mb={{ base: 10, md: 14 }} pb={{ base: 8, md: 10 }} borderBottomWidth="2px" borderColor="ink">
|
||||
<HStack gap="3" color="brand.700">
|
||||
<Box w="8" h="2px" bg="brand.500" />
|
||||
<Text fontSize="xs" fontWeight="700" letterSpacing="0.2em">
|
||||
BLOG
|
||||
</Text>
|
||||
</HStack>
|
||||
<Heading as="h1" fontSize={{ base: "4xl", md: "6xl" }} fontWeight="900" letterSpacing="-0.01em" color="ink">
|
||||
太陽能知識庫
|
||||
</Heading>
|
||||
<Text fontSize={{ base: "md", md: "lg" }} color="fg.muted" maxW="2xl">
|
||||
村屋太陽能嘅實用資訊、安裝心得同回本分析,幫你更了解點樣善用天台。
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<Text color="fg.muted">暫時未有文章,敬請期待。</Text>
|
||||
) : (
|
||||
<Stack gap={{ base: 10, md: 14 }}>
|
||||
{/* 最新文章 featured */}
|
||||
<Grid
|
||||
asChild
|
||||
templateColumns={{ base: "1fr", md: "1fr 1fr" }}
|
||||
gap={{ base: 5, md: 10 }}
|
||||
alignItems="center"
|
||||
role="group"
|
||||
cursor="pointer"
|
||||
>
|
||||
<a href={`/blog/${featured.slug}`}>
|
||||
{featured.coverImage && (
|
||||
<Box overflow="hidden">
|
||||
<Image
|
||||
src={featured.coverImage}
|
||||
alt={featured.title}
|
||||
w="full"
|
||||
aspectRatio="16 / 10"
|
||||
objectFit="cover"
|
||||
transition="transform .5s"
|
||||
_groupHover={{ transform: "scale(1.03)" }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Stack gap="3">
|
||||
<HStack gap="2" fontSize="xs" letterSpacing="0.1em" color="fg.muted" fontWeight="medium">
|
||||
<CalendarDays size={13} />
|
||||
{featured.dateLabel}
|
||||
</HStack>
|
||||
<Heading as="h2" fontSize={{ base: "xl", md: "3xl" }} fontWeight="900" color="ink" lineHeight="1.3" _groupHover={{ color: "brand.700" }} transition="color .2s">
|
||||
{featured.title}
|
||||
</Heading>
|
||||
{featured.excerpt && (
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.9">
|
||||
{featured.excerpt}
|
||||
</Text>
|
||||
)}
|
||||
<Text color="brand.700" fontWeight="600" fontSize="sm">
|
||||
閱讀更多 <ArrowRight size={14} style={{ display: "inline", verticalAlign: "-2px" }} />
|
||||
</Text>
|
||||
</Stack>
|
||||
</a>
|
||||
</Grid>
|
||||
|
||||
{/* 其餘文章:髮絲線清單 */}
|
||||
{rest.length > 0 && (
|
||||
<Stack gap="0" borderTopWidth="1px" borderColor="border.subtle">
|
||||
{rest.map((post) => (
|
||||
<Grid
|
||||
key={post.slug}
|
||||
asChild
|
||||
templateColumns={{ base: "1fr", md: "140px 1fr auto" }}
|
||||
gap={{ base: 2, md: 8 }}
|
||||
alignItems={{ md: "center" }}
|
||||
py={{ base: 5, md: 6 }}
|
||||
borderBottomWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
role="group"
|
||||
cursor="pointer"
|
||||
>
|
||||
<a href={`/blog/${post.slug}`}>
|
||||
<HStack gap="2" fontSize="xs" letterSpacing="0.08em" color="fg.muted" fontWeight="medium">
|
||||
<CalendarDays size={13} />
|
||||
{post.dateLabel}
|
||||
</HStack>
|
||||
<Stack gap="1">
|
||||
<Heading as="h2" fontSize={{ base: "lg", md: "xl" }} fontWeight="700" color="ink" lineHeight="1.4" _groupHover={{ color: "brand.700" }} transition="color .2s">
|
||||
{post.title}
|
||||
</Heading>
|
||||
{post.excerpt && (
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.7" lineClamp={2}>
|
||||
{post.excerpt}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
color="brand.700"
|
||||
opacity={{ base: 1, md: 0 }}
|
||||
transform={{ md: "translateX(-6px)" }}
|
||||
transition="all .2s"
|
||||
_groupHover={{ opacity: 1, transform: "translateX(0)" }}
|
||||
>
|
||||
<ArrowRight size={20} />
|
||||
</Box>
|
||||
</a>
|
||||
</Grid>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
<Footer settings={settings} />
|
||||
<WhatsAppFab settings={settings} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Box, Button, Container, Heading, Image, Link, Text, VStack } from "@chakra-ui/react";
|
||||
import { ArrowLeft, CalendarDays } from "lucide-react";
|
||||
import type { Settings } from "../../data/content";
|
||||
import { Footer } from "./Footer";
|
||||
import { Header } from "./Header";
|
||||
import { Provider } from "./Provider";
|
||||
import { WhatsAppFab } from "./WhatsAppFab";
|
||||
|
||||
const prose = {
|
||||
color: "#3B3835",
|
||||
fontSize: "1.05rem",
|
||||
lineHeight: 1.95,
|
||||
"& h2": { fontFamily: "'Noto Serif HK', serif", fontSize: "1.6rem", fontWeight: 700, mt: "2.5rem", mb: "0.75rem", color: "#1A1917", letterSpacing: "-0.01em" },
|
||||
"& h3": { fontFamily: "'Noto Serif HK', serif", fontSize: "1.25rem", fontWeight: 700, mt: "2rem", mb: "0.5rem", color: "#1A1917" },
|
||||
"& p": { mb: "1.25rem" },
|
||||
"& ul, & ol": { pl: "1.5rem", mb: "1.25rem" },
|
||||
"& li": { mb: "0.4rem" },
|
||||
"& a": { color: "#B45309", textDecoration: "underline", textUnderlineOffset: "3px" },
|
||||
"& strong": { fontWeight: 700, color: "#1A1917" },
|
||||
"& blockquote": { borderLeft: "3px solid #D97706", pl: "1rem", color: "#6E6862", my: "1.5rem" },
|
||||
"& img": { my: "1.5rem" },
|
||||
"& code": { bg: "#F3EFE7", px: "6px", py: "2px", fontSize: "0.9em" },
|
||||
"& pre": { bg: "#1A1917", color: "#FAF8F4", p: "1rem", overflowX: "auto", mb: "1.5rem" },
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
dateLabel: string;
|
||||
coverImage: string | null;
|
||||
html: string;
|
||||
settings: Settings;
|
||||
};
|
||||
|
||||
export function BlogPost({ title, dateLabel, coverImage, html, settings }: Props) {
|
||||
return (
|
||||
<Provider>
|
||||
<Header settings={settings} />
|
||||
<Box as="main" py={{ base: 10, md: 16 }}>
|
||||
<Container maxW="780px" px={{ base: 5, md: 8 }}>
|
||||
<Link
|
||||
href="/blog"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap="2"
|
||||
fontSize="sm"
|
||||
fontWeight="medium"
|
||||
color="fg.muted"
|
||||
mb="8"
|
||||
_hover={{ color: "brand.700", textDecoration: "none" }}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
返去 Blog
|
||||
</Link>
|
||||
|
||||
<VStack align="flex-start" gap="5" mb="10" pb="8" borderBottomWidth="2px" borderColor="ink">
|
||||
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="900" letterSpacing="-0.01em" lineHeight="1.25" color="ink">
|
||||
{title}
|
||||
</Heading>
|
||||
{dateLabel && (
|
||||
<Text fontSize="xs" letterSpacing="0.1em" fontWeight="medium" color="fg.muted">
|
||||
<CalendarDays size={14} style={{ display: "inline", marginRight: 6, verticalAlign: "-2px" }} />
|
||||
{dateLabel}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
{coverImage && (
|
||||
<Image src={coverImage} alt={title} w="full" mb="10" objectFit="cover" />
|
||||
)}
|
||||
|
||||
<Box css={prose} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
|
||||
<Button asChild variant="outline" mt="12" rounded="none" borderColor="ink" color="ink" _hover={{ bg: "ink", color: "white" }}>
|
||||
<a href="/blog">
|
||||
<ArrowLeft size={16} />
|
||||
睇更多文章
|
||||
</a>
|
||||
</Button>
|
||||
</Container>
|
||||
</Box>
|
||||
<Footer settings={settings} />
|
||||
<WhatsAppFab settings={settings} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Box, Grid, Heading, HStack, Image, Stack, Text } from "@chakra-ui/react";
|
||||
import { MapPin } from "lucide-react";
|
||||
import type { CaseStudy } from "../../db/schema";
|
||||
import { Section } from "./Section";
|
||||
import { SectionHeading } from "./SectionHeading";
|
||||
|
||||
export function Cases({ items }: { items: CaseStudy[] }) {
|
||||
if (items.length === 0) return null;
|
||||
const [featured, ...rest] = items;
|
||||
|
||||
return (
|
||||
<Section id="cases" tone="white">
|
||||
<SectionHeading
|
||||
eyebrow="完成案例"
|
||||
title="真實村屋工程案例"
|
||||
description="我哋為香港各區村屋提供專業太陽能方案,累積豐富施工經驗。"
|
||||
/>
|
||||
<Stack gap={{ base: 12, md: 16 }}>
|
||||
{/* 首案例 featured:雜誌式大圖排版 */}
|
||||
<Grid
|
||||
templateColumns={{ base: "1fr", lg: "1.2fr 0.8fr" }}
|
||||
gap={{ base: 5, lg: 10 }}
|
||||
alignItems="end"
|
||||
role="group"
|
||||
>
|
||||
<Box overflow="hidden">
|
||||
<Image
|
||||
src={featured.imageUrl ?? undefined}
|
||||
alt={featured.title}
|
||||
w="full"
|
||||
aspectRatio={{ base: "4 / 3", lg: "16 / 10" }}
|
||||
objectFit="cover"
|
||||
transition="transform .5s"
|
||||
_groupHover={{ transform: "scale(1.03)" }}
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap="3">
|
||||
<HStack gap="3" fontSize="xs" letterSpacing="0.1em" color="fg.muted" fontWeight="medium">
|
||||
<Text>{featured.completedAt}</Text>
|
||||
{featured.location && (
|
||||
<HStack gap="1" color="brand.700">
|
||||
<MapPin size={12} />
|
||||
<Text>{featured.location}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
<Heading as="h3" fontSize={{ base: "xl", md: "2xl" }} fontWeight="700" color="ink" lineHeight="1.35" _groupHover={{ color: "brand.700" }} transition="color .2s">
|
||||
{featured.title}
|
||||
</Heading>
|
||||
{featured.description && (
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.9">
|
||||
{featured.description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{rest.length > 0 && (
|
||||
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", lg: `repeat(${Math.min(rest.length, 3)}, 1fr)` }} gap={{ base: 10, md: 8 }}>
|
||||
{rest.map((item) => (
|
||||
<Stack key={item.id} gap="3" role="group" borderTopWidth="2px" borderColor="ink" pt="5">
|
||||
<Box overflow="hidden">
|
||||
<Image
|
||||
src={item.imageUrl ?? undefined}
|
||||
alt={item.title}
|
||||
w="full"
|
||||
aspectRatio="4 / 3"
|
||||
objectFit="cover"
|
||||
transition="transform .5s"
|
||||
_groupHover={{ transform: "scale(1.03)" }}
|
||||
/>
|
||||
</Box>
|
||||
<HStack gap="3" fontSize="xs" letterSpacing="0.1em" color="fg.muted" fontWeight="medium">
|
||||
<Text>{item.completedAt}</Text>
|
||||
{item.location && (
|
||||
<HStack gap="1" color="brand.700">
|
||||
<MapPin size={12} />
|
||||
<Text>{item.location}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
<Heading as="h3" fontSize="lg" fontWeight="700" color="ink" lineHeight="1.4" _groupHover={{ color: "brand.700" }} transition="color .2s">
|
||||
{item.title}
|
||||
</Heading>
|
||||
{item.description && (
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.8">
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Box, Button, Container, Grid, HStack, Heading, Stack, Text } from "@chakra-ui/react";
|
||||
import { Mail, MapPin, Phone } from "lucide-react";
|
||||
import { mailHref, telHref, whatsappHref, type Settings } from "../../data/content";
|
||||
import { WhatsAppIcon } from "./icons";
|
||||
|
||||
export function ContactCta({ settings }: { settings: Settings }) {
|
||||
const contact = [
|
||||
{ icon: Phone, label: settings.phone, href: telHref(settings) },
|
||||
{ icon: Mail, label: settings.email, href: mailHref(settings) },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box id="contact" bg="ink" color="white" py={{ base: 16, md: 24 }} scrollMarginTop="80px">
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
<Grid templateColumns={{ base: "1fr", lg: "1.2fr 0.8fr" }} gap={{ base: 10, lg: 16 }} alignItems="center">
|
||||
<Stack gap="6" align="flex-start">
|
||||
<HStack gap="3" color="brand.400">
|
||||
<Box w="8" h="2px" bg="brand.400" />
|
||||
<Text fontSize="xs" fontWeight="700" letterSpacing="0.2em">
|
||||
免費評估
|
||||
</Text>
|
||||
</HStack>
|
||||
<Heading as="h2" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="900" letterSpacing="-0.01em" lineHeight="1.2">
|
||||
立即預約免費評估
|
||||
</Heading>
|
||||
<Text fontSize={{ base: "md", md: "lg" }} color="whiteAlpha.700" maxW="xl">
|
||||
想知你嘅村屋天台啱唔啱裝太陽能?歡迎 WhatsApp 或致電,我哋會用清晰易明嘅方式為你詳答。
|
||||
</Text>
|
||||
<HStack gap="4" flexWrap="wrap" pt="2">
|
||||
<Button asChild size="lg" rounded="none" px="8" bg="brand.600" color="white" fontWeight="700" _hover={{ bg: "brand.500" }}>
|
||||
<a href={whatsappHref(settings)} target="_blank" rel="noopener">
|
||||
<WhatsAppIcon style={{ width: 18, height: 18 }} />
|
||||
WhatsApp 查詢
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" rounded="none" borderColor="whiteAlpha.500" color="white" size="lg" px="8" fontWeight="700" _hover={{ bg: "whiteAlpha.100" }}>
|
||||
<a href={telHref(settings)}>
|
||||
<Phone size={18} />
|
||||
致電我哋
|
||||
</a>
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="5" borderLeftWidth={{ lg: "1px" }} borderColor="whiteAlpha.200" pl={{ lg: 12 }}>
|
||||
{contact.map((c) => (
|
||||
<HStack key={c.label} gap="3" fontSize="sm" color="whiteAlpha.800">
|
||||
<Box color="brand.400">
|
||||
<c.icon size={16} />
|
||||
</Box>
|
||||
<Text>{c.label}</Text>
|
||||
</HStack>
|
||||
))}
|
||||
<HStack gap="3" align="flex-start" fontSize="sm" color="whiteAlpha.800">
|
||||
<Box color="brand.400" mt="1">
|
||||
<MapPin size={16} />
|
||||
</Box>
|
||||
<Text>{settings.address}</Text>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Accordion, Box, Text } from "@chakra-ui/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { ContentItem } from "../../db/schema";
|
||||
import { Section } from "./Section";
|
||||
import { SectionHeading } from "./SectionHeading";
|
||||
|
||||
export function Faq({ items }: { items: ContentItem[] }) {
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<Section id="faq">
|
||||
<SectionHeading
|
||||
eyebrow="常見問題"
|
||||
title="安裝前,你可能想知嘅事"
|
||||
description="仲有其他疑問?歡迎隨時 WhatsApp 問我哋。"
|
||||
/>
|
||||
<Box maxW="860px">
|
||||
<Accordion.Root collapsible defaultValue={["faq-0"]}>
|
||||
{items.map((item, index) => (
|
||||
<Accordion.Item
|
||||
key={item.id}
|
||||
value={`faq-${index}`}
|
||||
borderTopWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
_last={{ borderBottomWidth: "1px" }}
|
||||
>
|
||||
<Accordion.ItemTrigger py="6" px="0" _hover={{ bg: "transparent" }}>
|
||||
<Text flex="1" textAlign="start" fontFamily="heading" fontWeight="700" color="ink" fontSize={{ base: "md", md: "lg" }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Accordion.ItemIndicator color="brand.700" _open={{ transform: "rotate(45deg)" }}>
|
||||
<Plus size={18} />
|
||||
</Accordion.ItemIndicator>
|
||||
</Accordion.ItemTrigger>
|
||||
<Accordion.ItemContent>
|
||||
<Accordion.ItemBody px="0" pb="7" pt="0" color="fg.muted" fontSize="sm" lineHeight="1.9" maxW="2xl">
|
||||
{item.description}
|
||||
</Accordion.ItemBody>
|
||||
</Accordion.ItemContent>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</Box>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Box, Heading, SimpleGrid, Stack, Text } from "@chakra-ui/react";
|
||||
import type { ContentItem } from "../../db/schema";
|
||||
import { Section } from "./Section";
|
||||
import { SectionHeading } from "./SectionHeading";
|
||||
|
||||
export function Features({ items }: { items: ContentItem[] }) {
|
||||
return (
|
||||
<Section id="why" tone="subtle">
|
||||
<SectionHeading
|
||||
eyebrow="點解揀我哋"
|
||||
title="結合專業工程同貼心溝通"
|
||||
description="我哋唔止做工程,仲重視你由頭到尾嘅體驗同安心。"
|
||||
/>
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, sm: 2, lg: 3 }}
|
||||
gap="0"
|
||||
borderWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
bg="white"
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<Stack
|
||||
key={item.id}
|
||||
gap="3"
|
||||
p={{ base: 6, md: 8 }}
|
||||
borderTopWidth={{ base: i === 0 ? "0" : "1px", sm: i < 2 ? "0" : "1px", lg: i < 3 ? "0" : "1px" }}
|
||||
borderLeftWidth={{
|
||||
base: "0",
|
||||
sm: i % 2 === 1 ? "1px" : "0",
|
||||
lg: i % 3 === 0 ? "0" : "1px",
|
||||
}}
|
||||
borderColor="border.subtle"
|
||||
>
|
||||
<Text fontFamily="heading" fontWeight="700" fontSize="sm" letterSpacing="0.1em" color="brand.600">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</Text>
|
||||
<Heading as="h3" fontSize="lg" fontWeight="700" color="ink">
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.8">
|
||||
{item.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Box, Container, HStack, Link, Stack, Text, VStack } from "@chakra-ui/react";
|
||||
import { Mail, MapPin, Phone } from "lucide-react";
|
||||
import { mailHref, telHref, whatsappHref, type Settings } from "../../data/content";
|
||||
import { WhatsAppIcon } from "./icons";
|
||||
|
||||
export function Footer({ settings }: { settings: Settings }) {
|
||||
const name = settings.company_name || "盈豐太陽能工程有限公司";
|
||||
const shortName = settings.company_short_name || settings.company_name || "盈豐太陽能";
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<Box as="footer" bg="ink" color="white" borderTopWidth="1px" borderColor="whiteAlpha.200" pt={{ base: 12, md: 16 }} pb="8">
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
<Stack
|
||||
direction={{ base: "column", md: "row" }}
|
||||
justify="space-between"
|
||||
gap={{ base: 10, md: 16 }}
|
||||
>
|
||||
<VStack align="flex-start" gap="4" maxW="sm">
|
||||
<Text fontFamily="heading" fontWeight="900" fontSize="xl" letterSpacing="0.02em">
|
||||
{shortName}
|
||||
</Text>
|
||||
<Text color="whiteAlpha.600" fontSize="sm" lineHeight="1.8">
|
||||
專營香港村屋太陽能系統,由評估、設計、申請到安裝驗收,一站式為你善用天台,賺取發電回報。
|
||||
</Text>
|
||||
<Text color="whiteAlpha.500" fontSize="xs" letterSpacing="0.08em">
|
||||
{settings.license_no}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<HStack gap={{ base: 10, md: 16 }} align="flex-start">
|
||||
<VStack align="flex-start" gap="3">
|
||||
<Text fontWeight="700" fontSize="xs" letterSpacing="0.15em" color="whiteAlpha.500">
|
||||
快速連結
|
||||
</Text>
|
||||
{[
|
||||
{ label: "服務範圍", href: "#services" },
|
||||
{ label: "完成案例", href: "#cases" },
|
||||
{ label: "服務流程", href: "#process" },
|
||||
{ label: "常見問題", href: "#faq" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
].map((l) => (
|
||||
<Link
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
fontSize="sm"
|
||||
color="whiteAlpha.700"
|
||||
_hover={{ color: "white", textDecoration: "none" }}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</VStack>
|
||||
|
||||
<VStack align="flex-start" gap="3">
|
||||
<Text fontWeight="700" fontSize="xs" letterSpacing="0.15em" color="whiteAlpha.500">
|
||||
聯絡我哋
|
||||
</Text>
|
||||
<HStack gap="2" color="whiteAlpha.700" fontSize="sm">
|
||||
<Phone size={16} />
|
||||
<Link href={telHref(settings)} color="whiteAlpha.700" _hover={{ color: "white", textDecoration: "none" }}>
|
||||
{settings.phone}
|
||||
</Link>
|
||||
</HStack>
|
||||
<HStack gap="2" color="whiteAlpha.700" fontSize="sm">
|
||||
<WhatsAppIcon style={{ width: 16, height: 16 }} />
|
||||
<Link href={whatsappHref(settings)} target="_blank" rel="noopener" color="whiteAlpha.700" _hover={{ color: "white", textDecoration: "none" }}>
|
||||
WhatsApp
|
||||
</Link>
|
||||
</HStack>
|
||||
<HStack gap="2" color="whiteAlpha.700" fontSize="sm">
|
||||
<Mail size={16} />
|
||||
<Link href={mailHref(settings)} color="whiteAlpha.700" _hover={{ color: "white", textDecoration: "none" }}>
|
||||
{settings.email}
|
||||
</Link>
|
||||
</HStack>
|
||||
<HStack gap="2" color="whiteAlpha.700" fontSize="sm" align="flex-start">
|
||||
<MapPin size={16} style={{ marginTop: 3 }} />
|
||||
<Text>{settings.address}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
<Box borderTopWidth="1px" borderColor="whiteAlpha.200" mt="12" pt="6">
|
||||
<HStack justify="space-between" flexWrap="wrap" gap="2">
|
||||
<Text fontSize="xs" color="whiteAlpha.500">
|
||||
© {year} {name} All rights reserved.
|
||||
</Text>
|
||||
<Link href="/admin" fontSize="xs" color="whiteAlpha.500" _hover={{ color: "whiteAlpha.900", textDecoration: "none" }}>
|
||||
管理後台
|
||||
</Link>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CloseButton,
|
||||
Container,
|
||||
Drawer,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Link,
|
||||
Portal,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Menu, Phone } from "lucide-react";
|
||||
import { telHref, whatsappHref, type Settings } from "../../data/content";
|
||||
import { WhatsAppIcon } from "./icons";
|
||||
|
||||
const NAV = [
|
||||
{ label: "服務", href: "#services" },
|
||||
{ label: "完成案例", href: "#cases" },
|
||||
{ label: "服務流程", href: "#process" },
|
||||
{ label: "常見問題", href: "#faq" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
];
|
||||
|
||||
export function Header({ settings }: { settings: Settings }) {
|
||||
const name = settings.company_short_name || settings.company_name || "盈豐太陽能";
|
||||
const wa = whatsappHref(settings);
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="header"
|
||||
position="sticky"
|
||||
top="0"
|
||||
zIndex="sticky"
|
||||
bg="#FAF8F4"
|
||||
borderBottomWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
>
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
<Flex h="72px" align="center" justify="space-between" gap="4">
|
||||
<Stack asChild gap="0.5" cursor="pointer" lineHeight="1.2">
|
||||
<a href="/">
|
||||
<Text fontFamily="heading" fontWeight="900" fontSize="xl" letterSpacing="0.02em" color="ink">
|
||||
{name}
|
||||
</Text>
|
||||
<Text fontSize="11px" letterSpacing="0.12em" color="fg.muted" display={{ base: "none", sm: "block" }}>
|
||||
{settings.license_no || "村屋太陽能專家"}
|
||||
</Text>
|
||||
</a>
|
||||
</Stack>
|
||||
|
||||
<HStack gap="8" display={{ base: "none", lg: "flex" }}>
|
||||
{NAV.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
fontSize="sm"
|
||||
fontWeight="medium"
|
||||
color="ink"
|
||||
_hover={{ color: "brand.700", textDecoration: "none" }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</HStack>
|
||||
|
||||
<HStack gap="5">
|
||||
<Link
|
||||
href={telHref(settings)}
|
||||
display={{ base: "none", md: "flex" }}
|
||||
alignItems="center"
|
||||
gap="2"
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
color="ink"
|
||||
_hover={{ textDecoration: "none", color: "brand.700" }}
|
||||
>
|
||||
<Phone size={16} />
|
||||
{settings.phone}
|
||||
</Link>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
rounded="none"
|
||||
px="5"
|
||||
bg="ink"
|
||||
color="white"
|
||||
_hover={{ bg: "brand.700" }}
|
||||
display={{ base: "none", sm: "inline-flex" }}
|
||||
>
|
||||
<a href={wa} target="_blank" rel="noopener">
|
||||
<WhatsAppIcon style={{ width: 16, height: 16 }} />
|
||||
免費諮詢
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Drawer.Root>
|
||||
<Drawer.Trigger asChild>
|
||||
<IconButton aria-label="開啟選單" variant="ghost" display={{ base: "inline-flex", lg: "none" }}>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
</Drawer.Trigger>
|
||||
<Portal>
|
||||
<Drawer.Backdrop />
|
||||
<Drawer.Positioner>
|
||||
<Drawer.Content bg="#FAF8F4">
|
||||
<Drawer.Header borderBottomWidth="1px" borderColor="border.subtle">
|
||||
<Drawer.Title fontFamily="heading" fontWeight="900">{name}</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body>
|
||||
<Stack gap="1">
|
||||
{NAV.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
py="3"
|
||||
fontWeight="medium"
|
||||
color="ink"
|
||||
borderBottomWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
_hover={{ textDecoration: "none", color: "brand.700" }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</Drawer.Body>
|
||||
<Drawer.Footer pb="8">
|
||||
<Button asChild width="full" rounded="none" bg="ink" color="white" _hover={{ bg: "brand.700" }}>
|
||||
<a href={wa} target="_blank" rel="noopener">
|
||||
<WhatsAppIcon style={{ width: 18, height: 18 }} />
|
||||
WhatsApp 免費諮詢
|
||||
</a>
|
||||
</Button>
|
||||
</Drawer.Footer>
|
||||
<Drawer.CloseTrigger asChild>
|
||||
<CloseButton size="sm" />
|
||||
</Drawer.CloseTrigger>
|
||||
</Drawer.Content>
|
||||
</Drawer.Positioner>
|
||||
</Portal>
|
||||
</Drawer.Root>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Box, Button, Container, Grid, HStack, Heading, Image, Link, SimpleGrid, Stack, Text, VStack } from "@chakra-ui/react";
|
||||
import { ArrowRight, Check } from "lucide-react";
|
||||
import { whatsappHref, type Settings } from "../../data/content";
|
||||
import { WhatsAppIcon } from "./icons";
|
||||
|
||||
const TRUST = ["註冊電業承辦商", "舊客口碑介紹", "一對一專人跟進"];
|
||||
|
||||
export function Hero({ settings }: { settings: Settings }) {
|
||||
const stats = [
|
||||
{ label: "一般回本期", value: "約 6–9 年" },
|
||||
{ label: "系統可用年期", value: "20–25 年" },
|
||||
{ label: "牌照", value: settings.license_no || "註冊電業承辦商" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box pt={{ base: 12, md: 20 }} pb={{ base: 16, md: 24 }}>
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
<Grid templateColumns={{ base: "1fr", lg: "1.05fr 0.95fr" }} gap={{ base: 12, lg: 20 }} alignItems="center">
|
||||
<VStack align="flex-start" gap="7">
|
||||
<HStack gap="3" color="brand.700">
|
||||
<Box w="8" h="2px" bg="brand.500" />
|
||||
<Text fontSize="xs" fontWeight="700" letterSpacing="0.2em">
|
||||
{settings.hero_eyebrow || "香港村屋太陽能專家"}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Heading
|
||||
as="h1"
|
||||
fontSize={{ base: "4xl", sm: "5xl", md: "6xl" }}
|
||||
fontWeight="900"
|
||||
letterSpacing="-0.01em"
|
||||
lineHeight="1.15"
|
||||
color="ink"
|
||||
>
|
||||
{settings.hero_title}
|
||||
</Heading>
|
||||
|
||||
<Text fontSize={{ base: "md", md: "lg" }} color="fg.muted" maxW="xl">
|
||||
{settings.hero_subtitle}
|
||||
</Text>
|
||||
|
||||
<HStack gap="6" flexWrap="wrap" pt="1" align="center">
|
||||
<Button asChild size="lg" rounded="none" px="8" bg="ink" color="white" fontWeight="semibold" _hover={{ bg: "brand.700" }}>
|
||||
<a href={whatsappHref(settings)} target="_blank" rel="noopener">
|
||||
<WhatsAppIcon style={{ width: 18, height: 18 }} />
|
||||
{settings.hero_primary_cta || "免費預約評估"}
|
||||
</a>
|
||||
</Button>
|
||||
<Link
|
||||
href="#cases"
|
||||
fontWeight="semibold"
|
||||
color="ink"
|
||||
borderBottomWidth="2px"
|
||||
borderColor="brand.500"
|
||||
pb="0.5"
|
||||
_hover={{ color: "brand.700", textDecoration: "none" }}
|
||||
>
|
||||
{settings.hero_secondary_cta || "睇完成案例"} <ArrowRight size={15} style={{ display: "inline", verticalAlign: "-2px" }} />
|
||||
</Link>
|
||||
</HStack>
|
||||
|
||||
<HStack gap={{ base: "4", md: "6" }} flexWrap="wrap" pt="2">
|
||||
{TRUST.map((t) => (
|
||||
<HStack key={t} gap="2" color="fg.muted" fontSize="sm">
|
||||
<Box color="brand.600">
|
||||
<Check size={15} strokeWidth={3} />
|
||||
</Box>
|
||||
<Text>{t}</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
<Stack gap="0">
|
||||
<Image
|
||||
src={settings.hero_image}
|
||||
alt="村屋天台太陽能系統"
|
||||
w="full"
|
||||
aspectRatio="4 / 3"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<SimpleGrid
|
||||
columns={3}
|
||||
borderTopWidth="2px"
|
||||
borderColor="ink"
|
||||
pt="5"
|
||||
mt="6"
|
||||
gap="4"
|
||||
>
|
||||
{stats.map((s) => (
|
||||
<Stack key={s.label} gap="1">
|
||||
<Text fontSize="xs" letterSpacing="0.08em" color="fg.muted">
|
||||
{s.label}
|
||||
</Text>
|
||||
<Text fontFamily="heading" fontWeight="700" fontSize={{ base: "md", md: "lg" }} color="brand.700">
|
||||
{s.value}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { HomeData } from "../../data/content";
|
||||
import { Cases } from "./Cases";
|
||||
import { ContactCta } from "./ContactCta";
|
||||
import { Faq } from "./Faq";
|
||||
import { Features } from "./Features";
|
||||
import { Hero } from "./Hero";
|
||||
import { Process } from "./Process";
|
||||
import { Services } from "./Services";
|
||||
import { SiteChrome } from "./SiteChrome";
|
||||
import { TrustBar } from "./TrustBar";
|
||||
|
||||
export function HomePage({ data }: { data: HomeData }) {
|
||||
const { settings, services, features, steps, faqs, cases } = data;
|
||||
return (
|
||||
<SiteChrome settings={settings}>
|
||||
<Hero settings={settings} />
|
||||
<TrustBar />
|
||||
<Services items={services} />
|
||||
<Features items={features} />
|
||||
<Process items={steps} />
|
||||
<Cases items={cases} />
|
||||
<Faq items={faqs} />
|
||||
<ContactCta settings={settings} />
|
||||
</SiteChrome>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Box, SimpleGrid, Stack, Text } from "@chakra-ui/react";
|
||||
import type { ContentItem } from "../../db/schema";
|
||||
import { Section } from "./Section";
|
||||
import { SectionHeading } from "./SectionHeading";
|
||||
|
||||
export function Process({ items }: { items: ContentItem[] }) {
|
||||
return (
|
||||
<Section id="process">
|
||||
<SectionHeading
|
||||
eyebrow="服務流程"
|
||||
title="專業安裝流程 5 步到位"
|
||||
description="清晰透明嘅流程,每一步都有專人跟進,你唔需要自己四圍撲。"
|
||||
/>
|
||||
<Box>
|
||||
<SimpleGrid columns={{ base: 1, md: 5 }} gap={{ base: 10, md: 6 }}>
|
||||
{items.map((item) => (
|
||||
<Stack
|
||||
key={item.id}
|
||||
align="flex-start"
|
||||
textAlign="left"
|
||||
gap="3"
|
||||
>
|
||||
<Box
|
||||
fontFamily="heading"
|
||||
fontWeight="900"
|
||||
fontSize="5xl"
|
||||
lineHeight="1"
|
||||
color="brand.600"
|
||||
>
|
||||
{item.extra}
|
||||
</Box>
|
||||
<Text fontFamily="heading" fontWeight="700" fontSize="lg" color="ink">
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.75">
|
||||
{item.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { system } from "../../theme/system";
|
||||
|
||||
export function Provider({ children }: { children: ReactNode }) {
|
||||
return <ChakraProvider value={system}>{children}</ChakraProvider>;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Box, Container, type BoxProps } from "@chakra-ui/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type SectionProps = BoxProps & {
|
||||
id?: string;
|
||||
tone?: "paper" | "subtle" | "white";
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Section({ id, tone = "paper", children, ...rest }: SectionProps) {
|
||||
const bg = tone === "subtle" ? "bg.subtle" : tone === "white" ? "white" : "transparent";
|
||||
return (
|
||||
<Box
|
||||
as="section"
|
||||
id={id}
|
||||
bg={bg}
|
||||
py={{ base: 16, md: 24 }}
|
||||
scrollMarginTop="80px"
|
||||
{...rest}
|
||||
>
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Box, Heading, HStack, Stack, Text } from "@chakra-ui/react";
|
||||
|
||||
type Props = {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
align?: "center" | "start";
|
||||
};
|
||||
|
||||
export function SectionHeading({ eyebrow, title, description, align = "start" }: Props) {
|
||||
return (
|
||||
<Stack gap="5" textAlign={align} align={align === "center" ? "center" : "flex-start"} mb={{ base: 10, md: 14 }}>
|
||||
{eyebrow && (
|
||||
<HStack gap="3" color="brand.700">
|
||||
<Box w="8" h="2px" bg="brand.500" />
|
||||
<Text fontSize="xs" fontWeight="700" letterSpacing="0.2em">
|
||||
{eyebrow}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
<Heading
|
||||
as="h2"
|
||||
fontSize={{ base: "3xl", md: "5xl" }}
|
||||
fontWeight="900"
|
||||
letterSpacing="-0.01em"
|
||||
lineHeight="1.2"
|
||||
color="ink"
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
{description && (
|
||||
<Text fontSize={{ base: "md", md: "lg" }} color="fg.muted" maxW="2xl">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Box, Grid, Heading, Stack, Text } from "@chakra-ui/react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import type { ContentItem } from "../../db/schema";
|
||||
import { Section } from "./Section";
|
||||
import { SectionHeading } from "./SectionHeading";
|
||||
|
||||
export function Services({ items }: { items: ContentItem[] }) {
|
||||
return (
|
||||
<Section id="services">
|
||||
<SectionHeading
|
||||
eyebrow="服務範圍"
|
||||
title="專為村屋而設嘅太陽能方案"
|
||||
description="由天台評估到掛表發電,我哋提供真正一站式嘅村屋太陽能服務。"
|
||||
/>
|
||||
<Stack gap="0" borderTopWidth="1px" borderColor="border.subtle">
|
||||
{items.map((item, i) => (
|
||||
<Grid
|
||||
key={item.id}
|
||||
templateColumns={{ base: "1fr", md: "80px 1fr 2fr 40px" }}
|
||||
gap={{ base: 2, md: 8 }}
|
||||
alignItems={{ md: "center" }}
|
||||
py={{ base: 6, md: 8 }}
|
||||
borderBottomWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
role="group"
|
||||
>
|
||||
<Text
|
||||
fontFamily="heading"
|
||||
fontWeight="700"
|
||||
fontSize={{ base: "lg", md: "2xl" }}
|
||||
color="brand.600"
|
||||
>
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</Text>
|
||||
<Heading as="h3" fontSize={{ base: "xl", md: "2xl" }} fontWeight="700" color="ink" _groupHover={{ color: "brand.700" }} transition="color .2s">
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="1.8">
|
||||
{item.description}
|
||||
</Text>
|
||||
<Box
|
||||
color="brand.700"
|
||||
opacity="0"
|
||||
transform="translateX(-6px)"
|
||||
transition="all .2s"
|
||||
_groupHover={{ opacity: 1, transform: "translateX(0)" }}
|
||||
display={{ base: "none", md: "block" }}
|
||||
>
|
||||
<ArrowRight size={20} />
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Settings } from "../../data/content";
|
||||
import { Footer } from "./Footer";
|
||||
import { Header } from "./Header";
|
||||
import { Provider } from "./Provider";
|
||||
import { WhatsAppFab } from "./WhatsAppFab";
|
||||
|
||||
export function SiteChrome({ settings, children }: { settings: Settings; children: ReactNode }) {
|
||||
return (
|
||||
<Provider>
|
||||
<Header settings={settings} />
|
||||
<Box as="main">{children}</Box>
|
||||
<Footer settings={settings} />
|
||||
<WhatsAppFab settings={settings} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Box, Container, SimpleGrid, Stack, Text } from "@chakra-ui/react";
|
||||
import { BadgeCheck, FileCheck2, ShieldCheck, Timer } from "lucide-react";
|
||||
|
||||
const ITEMS = [
|
||||
{ icon: ShieldCheck, title: "註冊電業承辦商", desc: "合資格承辦,安全合法" },
|
||||
{ icon: FileCheck2, title: "上網電價 FiT 代辦", desc: "中電/港燈申請全程跟進" },
|
||||
{ icon: BadgeCheck, title: "認可人士 AP 簽發", desc: "C/PVS、WR1、GF1 文件" },
|
||||
{ icon: Timer, title: "20–25 年系統壽命", desc: "優質器材,長遠穩定" },
|
||||
];
|
||||
|
||||
export function TrustBar() {
|
||||
return (
|
||||
<Box bg="white" borderY="1px solid" borderColor="border.subtle">
|
||||
<Container maxW="1200px" px={{ base: 5, md: 8 }}>
|
||||
<SimpleGrid columns={{ base: 1, sm: 2, md: 4 }} gap="0">
|
||||
{ITEMS.map((item, i) => (
|
||||
<Stack
|
||||
key={item.title}
|
||||
direction="row"
|
||||
gap="4"
|
||||
align="flex-start"
|
||||
py={{ base: 5, md: 8 }}
|
||||
px={{ md: 6 }}
|
||||
borderTopWidth={{ base: i === 0 ? "0" : "1px", sm: i < 2 ? "0" : "1px", md: "0" }}
|
||||
borderLeftWidth={{ base: "0", sm: i % 2 === 1 ? "1px" : "0", md: i === 0 ? "0" : "1px" }}
|
||||
borderColor="border.subtle"
|
||||
>
|
||||
<Box color="brand.700" flexShrink="0" mt="0.5">
|
||||
<item.icon size={24} strokeWidth={1.5} />
|
||||
</Box>
|
||||
<Stack gap="0.5">
|
||||
<Text fontWeight="700" fontSize="sm" color="ink">
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{item.desc}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Link } from "@chakra-ui/react";
|
||||
import { whatsappHref, type Settings } from "../../data/content";
|
||||
import { WhatsAppIcon } from "./icons";
|
||||
|
||||
export function WhatsAppFab({ settings }: { settings: Settings }) {
|
||||
return (
|
||||
<Link
|
||||
href={whatsappHref(settings)}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
position="fixed"
|
||||
bottom={{ base: "5", md: "7" }}
|
||||
right={{ base: "5", md: "7" }}
|
||||
zIndex="dropdown"
|
||||
boxSize="56px"
|
||||
rounded="full"
|
||||
bg="ink"
|
||||
color="white"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
boxShadow="0 8px 24px rgba(26,25,23,0.3)"
|
||||
transition="transform .2s"
|
||||
_hover={{ transform: "scale(1.08)", textDecoration: "none", bg: "brand.700" }}
|
||||
aria-label="WhatsApp 查詢"
|
||||
>
|
||||
<WhatsAppIcon style={{ width: 28, height: 28 }} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function WhatsAppIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user