更新优化

This commit is contained in:
sofio
2026-02-04 11:57:19 +08:00
parent 30e0d480c2
commit 7de5db295f
31 changed files with 5497 additions and 103 deletions

BIN
Logo_Black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
Logo_White.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

4483
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@heroui/react": "^2.8.8",
"@next/third-parties": "^16.1.6",
"framer-motion": "^12.30.0",
"lucide-react": "^0.563.0",
"next": "16.1.6",

BIN
public/logo-footer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/logo-header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

BIN
public/wechat-qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { GoogleAnalytics } from "@next/third-parties/google";
import { routing } from "@/i18n/routing";
import { Locale } from "@/i18n/config";
import { getBaseUrl, ogImage } from "@/lib/seo";
@@ -78,6 +79,7 @@ export default async function LocaleLayout({ children, params }: Props) {
</Providers>
</NextIntlClientProvider>
</body>
<GoogleAnalytics gaId="G-ZC1KY3GRV0" />
</html>
);
}

View File

@@ -15,13 +15,18 @@ export async function generateMetadata({
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "metadata" });
return createPageMetadata({
const meta = createPageMetadata({
locale: locale as Locale,
pathname: "/",
title: t("homeTitle"),
title: t("title"),
description: t("homeDescription"),
siteName: t("title"),
});
return {
...meta,
title: { absolute: `${t("title")}${t("homeTitle")}` },
};
}
export default async function Home({

View File

@@ -6,6 +6,7 @@ import TechHeroSection from "@/components/TechHeroSection";
import ArchitectureSection from "@/components/ArchitectureSection";
import AICapabilitiesSection from "@/components/AICapabilitiesSection";
import Web3CapabilitiesSection from "@/components/Web3CapabilitiesSection";
import BlockchainSection from "@/components/BlockchainSection";
import TechCTASection from "@/components/TechCTASection";
export async function generateMetadata({
@@ -39,6 +40,7 @@ export default async function TechPage({
<ArchitectureSection />
<AICapabilitiesSection />
<Web3CapabilitiesSection />
<BlockchainSection />
<TechCTASection />
</main>
);

View File

@@ -0,0 +1,99 @@
import { NextRequest, NextResponse } from "next/server";
const LARK_WEBHOOK_URL =
"https://open.larksuite.com/open-apis/bot/v2/hook/6ac55b0c-e0d9-4932-9491-c09edb7acebb";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, company, email, phone, message } = body;
if (!name?.trim() || !email?.trim() || !message?.trim()) {
return NextResponse.json(
{ error: "Name, email and message are required" },
{ status: 400 }
);
}
const card = {
msg_type: "interactive",
card: {
header: {
title: {
tag: "plain_text",
content: "📩 新客户咨询",
},
template: "blue",
},
elements: [
{
tag: "div",
fields: [
{
is_short: true,
text: { tag: "lark_md", content: `**姓名**\n${name}` },
},
{
is_short: true,
text: {
tag: "lark_md",
content: `**公司**\n${company || "—"}`,
},
},
{
is_short: true,
text: { tag: "lark_md", content: `**邮箱**\n${email}` },
},
{
is_short: true,
text: {
tag: "lark_md",
content: `**电话**\n${phone || "—"}`,
},
},
],
},
{
tag: "div",
text: {
tag: "lark_md",
content: `**需求描述**\n${message || "—"}`,
},
},
{
tag: "hr",
},
{
tag: "note",
elements: [
{
tag: "plain_text",
content: `来源: 官网联系表单 | ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Hong_Kong" })}`,
},
],
},
],
},
};
const larkRes = await fetch(LARK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(card),
});
if (!larkRes.ok) {
return NextResponse.json(
{ error: "Failed to send message" },
{ status: 502 }
);
}
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -364,6 +364,122 @@ body {
gap: 16px;
}
/* ===== Blockchain / Smart Chain — Triangle Layout ===== */
.chain-layout {
display: flex;
align-items: center;
gap: 64px;
width: 100%;
}
.chain-triangle {
position: relative;
flex-shrink: 0;
width: 340px;
display: flex;
align-items: center;
justify-content: center;
padding: 28px 0;
}
.chain-triangle-svg {
width: 100%;
height: auto;
}
.chain-triangle-label {
position: absolute;
font-family: var(--f-sans);
font-size: 13px;
font-weight: 600;
color: var(--c-text-primary);
white-space: nowrap;
}
.chain-triangle-label--top {
top: 24px;
left: 50%;
transform: translateX(-50%);
}
.chain-triangle-label--bl {
bottom: 24px;
left: 5%;
transform: translateX(-50%);
}
.chain-triangle-label--br {
bottom: 24px;
left: 95%;
transform: translateX(-50%);
}
.chain-layers {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
}
.chain-layer-item {
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px 28px;
border: 1px solid var(--c-border);
}
.chain-layer-head {
display: flex;
align-items: center;
gap: 12px;
}
.chain-layer-num {
font-family: var(--f-mono);
font-size: 11px;
font-weight: 600;
color: var(--c-text-muted);
letter-spacing: 1px;
}
.chain-layer-name {
font-family: var(--f-serif);
font-size: 22px;
font-weight: 600;
color: var(--c-text-primary);
}
.chain-layer-rows {
display: flex;
flex-direction: column;
gap: 12px;
}
.chain-layer-row {
display: flex;
align-items: baseline;
gap: 12px;
}
.chain-layer-tag {
font-family: var(--f-sans);
font-size: 10px;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--c-text-muted);
background: #f2f0ec;
padding: 3px 8px;
flex-shrink: 0;
}
.chain-layer-text {
font-family: var(--f-sans);
font-size: 14px;
color: var(--c-text-secondary);
line-height: 1.6;
}
/* ===== Footer ===== */
.footer {
display: flex;
@@ -389,7 +505,6 @@ body {
}
.footer-logo-img {
filter: brightness(0) invert(1);
}
.footer-tagline {
@@ -417,6 +532,26 @@ body {
gap: 16px;
}
.footer-social-wechat-wrap {
position: relative;
}
.footer-wechat-popup {
position: fixed;
z-index: 9999;
background: #ffffff;
padding: 8px;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.footer-wechat-popup img {
display: block;
width: 160px;
height: 160px;
border-radius: 4px;
}
.footer-social-icon {
display: flex;
align-items: center;
@@ -1688,6 +1823,193 @@ body {
color: #cccccc;
}
/* ─── Contact Modal ──────────────────────────── */
/* HeroUI Modal overrides — strip rounded corners & shadow */
.cm-base {
border-radius: 0 !important;
background: var(--c-bg-light) !important;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12) !important;
padding: 0 !important;
overflow: hidden;
}
.cm-backdrop {
background: rgba(26, 26, 26, 0.45) !important;
backdrop-filter: blur(2px);
}
/* Inner layout */
.cm {
display: flex;
flex-direction: column;
padding: 32px;
gap: 0;
}
.cm-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.cm-header-left {
display: flex;
flex-direction: column;
gap: 8px;
}
.cm-tag {
font-family: var(--f-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 3px;
color: var(--c-text-muted);
}
.cm-title {
font-family: var(--f-serif);
font-size: 28px;
font-style: italic;
font-weight: 400;
letter-spacing: -1px;
color: var(--c-text-primary);
margin: 0;
}
.cm-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: 1px solid var(--c-border);
color: var(--c-text-secondary);
cursor: pointer;
transition: color 0.2s;
flex-shrink: 0;
}
.cm-close:hover {
color: var(--c-text-primary);
}
.cm-divider {
width: 100%;
height: 1px;
background: var(--c-border);
margin: 24px 0;
}
/* Form */
.cm-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.cm-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.cm-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.cm-label {
font-family: var(--f-sans);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
color: var(--c-text-muted);
}
.cm-required {
color: var(--c-text-secondary);
}
.cm-input,
.cm-textarea {
font-family: var(--f-sans);
font-size: 14px;
color: var(--c-text-primary);
background: transparent;
border: 1px solid var(--c-border);
padding: 12px 14px;
width: 100%;
outline: none;
transition: border-color 0.2s;
}
.cm-input::placeholder,
.cm-textarea::placeholder {
color: var(--c-text-muted);
font-size: 13px;
}
.cm-input:focus,
.cm-textarea:focus {
border-color: var(--c-text-secondary);
}
.cm-textarea {
resize: vertical;
min-height: 100px;
line-height: 1.6;
}
/* Footer */
.cm-footer {
padding-top: 24px;
}
.cm-btn {
width: 100%;
justify-content: center;
cursor: pointer;
border: none;
}
.cm-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.cm-btn.btn--outline-dark {
border: 1px solid var(--c-border);
}
/* Success state */
.cm-success {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
padding: 40px 0;
}
.cm-success-text {
font-family: var(--f-sans);
font-size: 15px;
line-height: 1.6;
color: var(--c-text-primary);
text-align: center;
margin: 0;
}
/* Error */
.cm-error {
font-family: var(--f-sans);
font-size: 13px;
color: #c62828;
margin: 0;
}
/* ─── Language Switcher ──────────────────────────── */
.lang-switcher-trigger {
font-size: 12px !important;
@@ -1790,6 +2112,10 @@ body {
.flow-arrow { transform: rotate(90deg); padding: 8px 0; }
.flow-card { width: 100%; max-width: 360px; }
/* ── Chain layout ── */
.chain-layout { gap: 40px; }
.chain-triangle { width: 280px; }
/* ── Scenario ── */
.scenario-content { flex-direction: column; gap: 32px; }
@@ -1964,4 +2290,20 @@ body {
/* ── Scenario metrics ── */
.scenario-metrics { flex-direction: column; gap: 16px; }
/* ── Chain layout ── */
.chain-layout {
flex-direction: column;
gap: 32px;
align-items: center;
}
.chain-triangle { width: 220px; }
.chain-layer-item { padding: 20px; }
.chain-layer-name { font-size: 18px; }
/* ── Contact Modal ── */
.cm { padding: 24px; }
.cm-title { font-size: 24px; }
.cm-row { grid-template-columns: 1fr; gap: 16px; }
.cm-divider { margin: 20px 0; }
}

View File

@@ -18,7 +18,11 @@ export default async function AboutHeroSection() {
return (
<section className="about-hero-section">
<div className="about-hero-content">
<span className="about-hero-label">{t("label")}</span>
<div className="decorated-tag">
<span className="decorated-tag-line" />
<span className="decorated-tag-text">{t("label")}</span>
<span className="decorated-tag-line" />
</div>
<h1 className="about-hero-title">{t("title")}</h1>
<p className="about-hero-subtitle">{t("subtitle")}</p>
<div className="about-hero-vision">

View File

@@ -0,0 +1,83 @@
import { getTranslations } from "next-intl/server";
export default async function BlockchainSection() {
const t = await getTranslations("blockchain");
const layers = [
{
name: t("layer1Name"),
func: t("layer1Func"),
value: t("layer1Value"),
},
{
name: t("layer2Name"),
func: t("layer2Func"),
value: t("layer2Value"),
},
{
name: t("layer3Name"),
func: t("layer3Func"),
value: t("layer3Value"),
},
];
return (
<section className="section section--light">
<div className="cap-header">
<div className="cap-header-left">
<div className="cap-divider">
<span className="cap-divider-line" />
<span className="cap-divider-label">{t("label")}</span>
</div>
<h2 className="cap-title">{t("title")}</h2>
<p className="cap-subtitle">{t("subtitle")}</p>
</div>
<div className="cap-header-right">
<span className="cap-big-number">{t("layerCount")}</span>
<span className="cap-metric-label">{t("layerLabel")}</span>
</div>
</div>
<div className="chain-layout">
<div className="chain-triangle">
<svg viewBox="0 0 200 180" className="chain-triangle-svg">
<polygon
points="100,10 10,170 190,170"
fill="var(--c-bg-dark)"
/>
</svg>
<span className="chain-triangle-label chain-triangle-label--top">
{layers[0].name}
</span>
<span className="chain-triangle-label chain-triangle-label--bl">
{layers[1].name}
</span>
<span className="chain-triangle-label chain-triangle-label--br">
{layers[2].name}
</span>
</div>
<div className="chain-layers">
{layers.map((layer, i) => (
<div key={layer.name} className="chain-layer-item">
<div className="chain-layer-head">
<span className="chain-layer-num">{`0${i + 1}`}</span>
<span className="chain-layer-name">{layer.name}</span>
</div>
<div className="chain-layer-rows">
<div className="chain-layer-row">
<span className="chain-layer-tag">{t("colFunc")}</span>
<span className="chain-layer-text">{layer.func}</span>
</div>
<div className="chain-layer-row">
<span className="chain-layer-tag">{t("colValue")}</span>
<span className="chain-layer-text">{layer.value}</span>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,6 +1,7 @@
import { getTranslations } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import { ArrowRight } from "lucide-react";
import ContactButton from "./ContactButton";
export default async function CTASection() {
const t = await getTranslations("cta");
@@ -13,9 +14,9 @@ export default async function CTASection() {
<span>{t("services")}</span>
<ArrowRight size={16} />
</Link>
<Link href="/contact" className="btn btn--outline-dark">
<ContactButton className="btn btn--outline-dark">
{t("contact")}
</Link>
</ContactButton>
</div>
</section>
);

View File

@@ -0,0 +1,27 @@
"use client";
import { useDisclosure } from "@heroui/react";
import ContactModal from "./ContactModal";
interface ContactButtonProps {
className?: string;
style?: React.CSSProperties;
children: React.ReactNode;
}
export default function ContactButton({
className,
style,
children,
}: ContactButtonProps) {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<button className={className} style={style} onClick={onOpen}>
{children}
</button>
<ContactModal isOpen={isOpen} onOpenChange={onOpenChange} />
</>
);
}

View File

@@ -0,0 +1,181 @@
"use client";
import { useState } from "react";
import { Modal, ModalContent } from "@heroui/react";
import { useTranslations } from "next-intl";
import { X } from "lucide-react";
interface ContactModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}
export default function ContactModal({
isOpen,
onOpenChange,
}: ContactModalProps) {
const t = useTranslations("contact");
const [form, setForm] = useState({
name: "",
company: "",
email: "",
phone: "",
message: "",
});
const [status, setStatus] = useState<
"idle" | "loading" | "success" | "error"
>("idle");
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = async () => {
if (!form.name.trim() || !form.email.trim() || !form.message.trim()) return;
setStatus("loading");
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) throw new Error("Failed");
setStatus("success");
setForm({ name: "", company: "", email: "", phone: "", message: "" });
} catch {
setStatus("error");
}
};
const handleClose = (open: boolean) => {
if (!open) setStatus("idle");
onOpenChange(open);
};
const canSubmit = form.name.trim() && form.email.trim() && form.message.trim();
return (
<Modal
isOpen={isOpen}
onOpenChange={handleClose}
size="lg"
placement="center"
hideCloseButton
classNames={{
backdrop: "cm-backdrop",
wrapper: "cm-wrapper",
base: "cm-base",
}}
>
<ModalContent>
{(onClose) => (
<div className="cm">
{/* Header */}
<div className="cm-header">
<div className="cm-header-left">
<span className="cm-tag">CONTACT</span>
<h2 className="cm-title">{t("title")}</h2>
</div>
<button className="cm-close" onClick={onClose} aria-label="Close">
<X size={18} />
</button>
</div>
<div className="cm-divider" />
{/* Body */}
{status === "success" ? (
<div className="cm-success">
<p className="cm-success-text">{t("success")}</p>
<button className="btn btn--outline-dark cm-btn" onClick={onClose}>
{t("close")}
</button>
</div>
) : (
<>
<div className="cm-form">
<div className="cm-row">
<div className="cm-field">
<label className="cm-label">
{t("name")} <span className="cm-required">*</span>
</label>
<input
className="cm-input"
name="name"
value={form.name}
onChange={handleChange}
placeholder={t("namePlaceholder")}
/>
</div>
<div className="cm-field">
<label className="cm-label">{t("company")}</label>
<input
className="cm-input"
name="company"
value={form.company}
onChange={handleChange}
placeholder={t("companyPlaceholder")}
/>
</div>
</div>
<div className="cm-row">
<div className="cm-field">
<label className="cm-label">
{t("email")} <span className="cm-required">*</span>
</label>
<input
className="cm-input"
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder={t("emailPlaceholder")}
/>
</div>
<div className="cm-field">
<label className="cm-label">{t("phone")}</label>
<input
className="cm-input"
name="phone"
value={form.phone}
onChange={handleChange}
placeholder={t("phonePlaceholder")}
/>
</div>
</div>
<div className="cm-field">
<label className="cm-label">
{t("message")} <span className="cm-required">*</span>
</label>
<textarea
className="cm-textarea"
name="message"
value={form.message}
onChange={handleChange}
placeholder={t("messagePlaceholder")}
rows={4}
/>
</div>
{status === "error" && (
<p className="cm-error">{t("error")}</p>
)}
</div>
<div className="cm-footer">
<button
className="btn btn--dark cm-btn"
onClick={handleSubmit}
disabled={!canSubmit || status === "loading"}
>
{status === "loading" ? t("submitting") : t("submit")}
</button>
</div>
</>
)}
</div>
)}
</ModalContent>
</Modal>
);
}

View File

@@ -27,7 +27,7 @@ export default async function FlywheelSection() {
},
];
const summarySteps = [t("summary1"), t("summary2"), t("summary3")];
const summarySteps = [t("summary1"), t("summary2")];
return (
<section className="section section--dark">

View File

@@ -1,44 +1,6 @@
import { Link } from "@/i18n/navigation";
import { getTranslations } from "next-intl/server";
function WeChatIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05a6.329 6.329 0 0 1-.235-1.69c0-3.542 3.276-6.426 7.315-6.426.348 0 .688.029 1.023.074C16.088 4.68 12.727 2.189 8.691 2.189zM5.785 5.991a1.09 1.09 0 0 1 1.083 1.09 1.09 1.09 0 0 1-1.083 1.09A1.09 1.09 0 0 1 4.7 7.08a1.09 1.09 0 0 1 1.085-1.09zm5.88 0a1.09 1.09 0 0 1 1.083 1.09 1.09 1.09 0 0 1-1.083 1.09 1.09 1.09 0 0 1-1.085-1.09 1.09 1.09 0 0 1 1.085-1.09zm2.927 3.525c-3.508 0-6.36 2.51-6.36 5.596 0 3.088 2.852 5.596 6.36 5.596a7.5 7.5 0 0 0 2.36-.382.636.636 0 0 1 .527.074l1.402.822a.244.244 0 0 0 .122.04.214.214 0 0 0 .213-.217c0-.053-.02-.105-.035-.156l-.286-1.09a.432.432 0 0 1 .156-.488c1.352-.998 2.22-2.465 2.22-4.1 0-3.087-2.853-5.596-6.36-5.596h-.319zm-1.834 2.89a.905.905 0 0 1 .9.907.905.905 0 0 1-.9.906.905.905 0 0 1-.902-.906.905.905 0 0 1 .902-.906zm3.99 0a.905.905 0 0 1 .9.907.905.905 0 0 1-.9.906.905.905 0 0 1-.902-.906.905.905 0 0 1 .901-.906z" />
</svg>
);
}
function LinkedInIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
);
}
function XIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</svg>
);
}
function MailIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
);
}
const socialIcons = [
{ icon: WeChatIcon, label: "WeChat" },
{ icon: LinkedInIcon, label: "LinkedIn" },
{ icon: XIcon, label: "X" },
{ icon: MailIcon, label: "Email" },
];
import FooterSocial from "./FooterSocial";
export default async function Footer() {
const t = await getTranslations("footer");
@@ -63,7 +25,7 @@ export default async function Footer() {
<div className="footer-brand">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/logo.png"
src="/logo-footer.png"
alt="DESUN SINGULARITY"
width={160}
height={46}
@@ -72,18 +34,7 @@ export default async function Footer() {
<p className="footer-tagline">{t("tagline")}</p>
<div className="footer-contact">
<span className="footer-contact-label">{t("contactLabel")}</span>
<div className="footer-social">
{socialIcons.map((item) => (
<a
key={item.label}
href="#"
className="footer-social-icon"
aria-label={item.label}
>
<item.icon size={16} />
</a>
))}
</div>
<FooterSocial />
</div>
</div>

View File

@@ -0,0 +1,112 @@
"use client";
import { useRef, useState } from "react";
function WeChatIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05a6.329 6.329 0 0 1-.235-1.69c0-3.542 3.276-6.426 7.315-6.426.348 0 .688.029 1.023.074C16.088 4.68 12.727 2.189 8.691 2.189zM5.785 5.991a1.09 1.09 0 0 1 1.083 1.09 1.09 1.09 0 0 1-1.083 1.09A1.09 1.09 0 0 1 4.7 7.08a1.09 1.09 0 0 1 1.085-1.09zm5.88 0a1.09 1.09 0 0 1 1.083 1.09 1.09 1.09 0 0 1-1.083 1.09 1.09 1.09 0 0 1-1.085-1.09 1.09 1.09 0 0 1 1.085-1.09zm2.927 3.525c-3.508 0-6.36 2.51-6.36 5.596 0 3.088 2.852 5.596 6.36 5.596a7.5 7.5 0 0 0 2.36-.382.636.636 0 0 1 .527.074l1.402.822a.244.244 0 0 0 .122.04.214.214 0 0 0 .213-.217c0-.053-.02-.105-.035-.156l-.286-1.09a.432.432 0 0 1 .156-.488c1.352-.998 2.22-2.465 2.22-4.1 0-3.087-2.853-5.596-6.36-5.596h-.319zm-1.834 2.89a.905.905 0 0 1 .9.907.905.905 0 0 1-.9.906.905.905 0 0 1-.902-.906.905.905 0 0 1 .902-.906zm3.99 0a.905.905 0 0 1 .9.907.905.905 0 0 1-.9.906.905.905 0 0 1-.902-.906.905.905 0 0 1 .901-.906z" />
</svg>
);
}
function LinkedInIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
);
}
function XIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</svg>
);
}
function MailIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
);
}
export default function FooterSocial() {
const [showQr, setShowQr] = useState(false);
const btnRef = useRef<HTMLButtonElement>(null);
const getPopupStyle = (): React.CSSProperties => {
if (!btnRef.current) return {};
const rect = btnRef.current.getBoundingClientRect();
return {
left: rect.left + rect.width / 2 - 88,
top: rect.top - 176 - 12,
};
};
return (
<div className="footer-social">
{/* WeChat — hover/click shows QR */}
<div
className="footer-social-wechat-wrap"
onMouseEnter={() => setShowQr(true)}
onMouseLeave={() => setShowQr(false)}
>
<button
ref={btnRef}
className="footer-social-icon"
aria-label="WeChat"
onClick={() => setShowQr((v) => !v)}
>
<WeChatIcon size={16} />
</button>
{showQr && (
<div className="footer-wechat-popup" style={getPopupStyle()}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/wechat-qr.png"
alt="WeChat QR Code"
width={160}
height={160}
/>
</div>
)}
</div>
{/* LinkedIn */}
<a
href="https://www.linkedin.com/company/desunsingularity"
className="footer-social-icon"
aria-label="LinkedIn"
target="_blank"
rel="noopener noreferrer"
>
<LinkedInIcon size={16} />
</a>
{/* X */}
<a
href="https://x.com/DesunHK"
className="footer-social-icon"
aria-label="X"
target="_blank"
rel="noopener noreferrer"
>
<XIcon size={16} />
</a>
{/* Email */}
<a
href="mailto:contact@desunweb3.com"
className="footer-social-icon"
aria-label="Email"
>
<MailIcon size={16} />
</a>
</div>
);
}

View File

@@ -4,13 +4,16 @@ import Image from "next/image";
import { Link, usePathname } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { useDisclosure } from "@heroui/react";
import { Menu, X } from "lucide-react";
import LanguageSwitcher from "./LanguageSwitcher";
import ContactModal from "./ContactModal";
export default function Header() {
const pathname = usePathname();
const t = useTranslations("header");
const [mobileOpen, setMobileOpen] = useState(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const navItems = [
{ label: t("home"), href: "/" as const },
@@ -23,7 +26,7 @@ export default function Header() {
<header className="header">
<Link href="/" className="logo">
<Image
src="/logo.png"
src="/logo-header.png"
alt="DESUN SINGULARITY"
width={140}
height={40}
@@ -45,9 +48,9 @@ export default function Header() {
<div className="header-right">
<LanguageSwitcher />
<Link href="/contact" className="contact-btn">
<button className="contact-btn" onClick={onOpen}>
{t("contact")}
</Link>
</button>
</div>
{/* Mobile hamburger */}
@@ -75,17 +78,20 @@ export default function Header() {
))}
<div className="mobile-nav-footer">
<LanguageSwitcher />
<Link
href="/contact"
<button
className="contact-btn"
onClick={() => setMobileOpen(false)}
onClick={() => {
setMobileOpen(false);
onOpen();
}}
>
{t("contact")}
</Link>
</button>
</div>
</nav>
</div>
)}
<ContactModal isOpen={isOpen} onOpenChange={onOpenChange} />
</header>
);
}

View File

@@ -13,7 +13,11 @@ export default async function HeroSection() {
return (
<section className="section section--light hero">
<div className="hero-content">
<span className="label-text">{t("label")}</span>
<div className="decorated-tag">
<span className="decorated-tag-line" />
<span className="decorated-tag-text">{t("label")}</span>
<span className="decorated-tag-line" />
</div>
<h1 className="heading-display">{t("title")}</h1>
<p className="body-text body-text--lg">{t("subtitle")}</p>
</div>

View File

@@ -32,14 +32,6 @@ export default async function RecruitSection() {
))}
</div>
</div>
<div className="recruit-right">
<span className="recruit-col-label">{t("newsLabel")}</span>
<div className="recruit-news-placeholder">
<span style={{ fontSize: 32 }}>📰</span>
<span className="recruit-news-text">{t("newsPlaceholder")}</span>
<span className="recruit-news-desc">{t("newsDesc")}</span>
</div>
</div>
</div>
</section>
);

View File

@@ -1,5 +1,5 @@
import { getTranslations } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import ContactButton from "./ContactButton";
export default async function SolutionsCTASection() {
const t = await getTranslations("solCta");
@@ -9,10 +9,10 @@ export default async function SolutionsCTASection() {
<div className="sol-cta-divider" />
<h2 className="heading-section heading-section--dark">{t("title")}</h2>
<p className="body-text body-text--lg sol-cta-desc">{t("desc")}</p>
<Link href="/contact" className="btn btn--dark" style={{ gap: 8 }}>
<ContactButton className="btn btn--dark" style={{ gap: 8 }}>
<span>{t("contact")}</span>
<span></span>
</Link>
</ContactButton>
</section>
);
}

View File

@@ -30,7 +30,11 @@ export default async function SolutionsHeroSection() {
return (
<section className="section section--light">
<div className="section-header" style={{ gap: 24 }}>
<span className="label-text">{t("label")}</span>
<div className="decorated-tag">
<span className="decorated-tag-line" />
<span className="decorated-tag-text">{t("label")}</span>
<span className="decorated-tag-line" />
</div>
<h1 className="sol-hero-title">{t("title")}</h1>
<h2 className="sol-hero-subtitle">{t("subtitle")}</h2>
<p className="body-text body-text--lg sol-hero-desc">{t("desc")}</p>

View File

@@ -1,5 +1,6 @@
import { getTranslations } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import ContactButton from "./ContactButton";
export default async function TechCTASection() {
const t = await getTranslations("techCta");
@@ -11,9 +12,9 @@ export default async function TechCTASection() {
<Link href="/solutions" className="btn btn--dark">
{t("solutions")}
</Link>
<Link href="/contact" className="btn btn--outline-dark">
<ContactButton className="btn btn--outline-dark">
{t("contact")}
</Link>
</ContactButton>
</div>
</section>
);

View File

@@ -29,9 +29,9 @@
"metric2Label": "Operating Area",
"metric2Value": "14M+ m²",
"metric2Desc": "Covering residential, commercial & industrial parks",
"metric3Label": "Strategy APY",
"metric3Value": "18%+",
"metric3Desc": "Market-neutral strategy performance",
"metric3Label": "Strategy Portfolio",
"metric3Value": "10+",
"metric3Desc": "Diversified quantitative strategy matrix",
"metric4Label": "Licenses",
"metric4Value": "Type 1/2/4/5/9",
"metric4Desc": "Hong Kong SFC licenses"
@@ -61,8 +61,7 @@
"card3Feature2": "AI risk management",
"card3Feature3": "Multi-market arbitrage",
"summary1": "Create value in physical spaces",
"summary2": "Discover quality assets and connect global capital",
"summary3": "Amplify returns with quantitative strategies"
"summary2": "Discover quality assets and connect global capital"
},
"market": {
"label": "Market Opportunity",
@@ -345,11 +344,48 @@
"card4Desc": "Hong Kong SFC framework, full license coverage",
"card4Value": "Type 1/2/4/5/9 Licenses"
},
"blockchain": {
"label": "BLOCKCHAIN",
"title": "DeSpace Smart Chain",
"subtitle": "The digital operating system for urban spaces, the trust foundation for asset value",
"layerCount": "3",
"layerLabel": "Three-Layer Value Architecture",
"colLayer": "LAYER",
"colFunc": "FUNCTION",
"colValue": "VALUE",
"layer1Name": "Data Layer",
"layer1Func": "Unified storage of urban space operational data",
"layer1Value": "Authentic, complete, immutable",
"layer2Name": "Rights Layer",
"layer2Func": "On-chain asset rights confirmation and certification",
"layer2Value": "Trusted data source for RWA issuance",
"layer3Name": "Trust Layer",
"layer3Func": "Transparent multi-party collaboration network",
"layer3Value": "Eliminate trust friction, reduce transaction costs"
},
"techCta": {
"title": "Explore How Our Technology Empowers Your Business",
"solutions": "View Business Scenarios",
"contact": "Contact Us"
},
"contact": {
"title": "Contact Us",
"name": "Name",
"namePlaceholder": "Enter your name",
"company": "Company",
"companyPlaceholder": "Enter company name",
"email": "Email",
"emailPlaceholder": "Enter email address",
"phone": "Phone",
"phonePlaceholder": "Enter phone number",
"message": "Message",
"messagePlaceholder": "Briefly describe your needs",
"submit": "Submit",
"submitting": "Submitting...",
"success": "Submitted successfully! We will contact you shortly.",
"error": "Submission failed. Please try again later.",
"close": "Close"
},
"footer": {
"tagline": "AI + Web3 Powered Full-Chain Asset Technology Platform",
"contactLabel": "Contact Us",

View File

@@ -29,9 +29,9 @@
"metric2Label": "運營面積",
"metric2Value": "1400萬+ m²",
"metric2Desc": "覆蓋住宅、商業、產業園",
"metric3Label": "策略年化",
"metric3Value": "18%+",
"metric3Desc": "市場中性策略表現",
"metric3Label": "策略組合",
"metric3Value": "10+",
"metric3Desc": "多元量化策略矩陣",
"metric4Label": "合規牌照",
"metric4Value": "1/2/4/5/9 類",
"metric4Desc": "香港證監會牌照"
@@ -61,8 +61,7 @@
"card3Feature2": "AI 風控系統",
"card3Feature3": "多市場套利",
"summary1": "在實體空間創造價值",
"summary2": "發現優質資產連接全球資本",
"summary3": "以量化策略放大收益"
"summary2": "發現優質資產連接全球資本"
},
"market": {
"label": "市場機會",
@@ -345,11 +344,48 @@
"card4Desc": "香港 SFC 框架,全牌照覆蓋",
"card4Value": "1/2/4/5/9 類牌照"
},
"blockchain": {
"label": "BLOCKCHAIN",
"title": "智能鏈 (DeSpace Smart Chain)",
"subtitle": "城市空間的數字操作系統,資產價值的互信底座",
"layerCount": "3",
"layerLabel": "三層價值架構",
"colLayer": "層級",
"colFunc": "功能",
"colValue": "價值",
"layer1Name": "數據層",
"layer1Func": "城市空間運營數據的統一存儲",
"layer1Value": "真實、完整、不可篡改",
"layer2Name": "確權層",
"layer2Func": "資產權益的鏈上確權與存證",
"layer2Value": "為 RWA 發行提供可信數據源",
"layer3Name": "互信層",
"layer3Func": "多方參與的透明協作網絡",
"layer3Value": "消除信任摩擦,降低交易成本"
},
"techCta": {
"title": "探索我們的技術如何賦能您的業務",
"solutions": "查看業務場景",
"contact": "聯繫我們"
},
"contact": {
"title": "聯繫我們",
"name": "姓名",
"namePlaceholder": "請輸入您的姓名",
"company": "公司",
"companyPlaceholder": "請輸入公司名稱",
"email": "郵箱",
"emailPlaceholder": "請輸入郵箱地址",
"phone": "電話",
"phonePlaceholder": "請輸入聯繫電話",
"message": "需求描述",
"messagePlaceholder": "請簡要描述您的需求",
"submit": "提交",
"submitting": "提交中...",
"success": "提交成功,我們會盡快與您聯繫!",
"error": "提交失敗,請稍後重試",
"close": "關閉"
},
"footer": {
"tagline": "AI + Web3 驅動的全鏈路資產科技平台",
"contactLabel": "聯繫我們",

View File

@@ -29,9 +29,9 @@
"metric2Label": "运营面积",
"metric2Value": "1400万+ m²",
"metric2Desc": "覆盖住宅、商业、产业园",
"metric3Label": "策略年化",
"metric3Value": "18%+",
"metric3Desc": "市场中性策略表现",
"metric3Label": "策略组合",
"metric3Value": "10+",
"metric3Desc": "多元量化策略矩阵",
"metric4Label": "合规牌照",
"metric4Value": "1/2/4/5/9 类",
"metric4Desc": "香港证监会牌照"
@@ -61,8 +61,7 @@
"card3Feature2": "AI 风控系统",
"card3Feature3": "多市场套利",
"summary1": "在实体空间创造价值",
"summary2": "发现优质资产连接全球资本",
"summary3": "以量化策略放大收益"
"summary2": "发现优质资产连接全球资本"
},
"market": {
"label": "市场机会",
@@ -345,11 +344,48 @@
"card4Desc": "香港 SFC 框架,全牌照覆盖",
"card4Value": "1/2/4/5/9 类牌照"
},
"blockchain": {
"label": "BLOCKCHAIN",
"title": "智能链 (DeSpace Smart Chain)",
"subtitle": "城市空间的数字操作系统,资产价值的互信底座",
"layerCount": "3",
"layerLabel": "三层价值架构",
"colLayer": "层级",
"colFunc": "功能",
"colValue": "价值",
"layer1Name": "数据层",
"layer1Func": "城市空间运营数据的统一存储",
"layer1Value": "真实、完整、不可篡改",
"layer2Name": "确权层",
"layer2Func": "资产权益的链上确权与存证",
"layer2Value": "为 RWA 发行提供可信数据源",
"layer3Name": "互信层",
"layer3Func": "多方参与的透明协作网络",
"layer3Value": "消除信任摩擦,降低交易成本"
},
"techCta": {
"title": "探索我们的技术如何赋能您的业务",
"solutions": "查看业务场景",
"contact": "联系我们"
},
"contact": {
"title": "联系我们",
"name": "姓名",
"namePlaceholder": "请输入您的姓名",
"company": "公司",
"companyPlaceholder": "请输入公司名称",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱地址",
"phone": "电话",
"phonePlaceholder": "请输入联系电话",
"message": "需求描述",
"messagePlaceholder": "请简要描述您的需求",
"submit": "提交",
"submitting": "提交中...",
"success": "提交成功,我们会尽快与您联系!",
"error": "提交失败,请稍后重试",
"close": "关闭"
},
"footer": {
"tagline": "AI + Web3 驱动的全链路资产科技平台",
"contactLabel": "联系我们",