更新样式,基本雏形出现
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.18.1",
|
||||
|
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.0
|
||||
version: 1.2.3(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-toast':
|
||||
specifier: ^1.2.14
|
||||
version: 1.2.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.1
|
||||
@@ -684,6 +687,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-toast@1.2.14':
|
||||
resolution: {integrity: sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
@@ -2488,6 +2504,26 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
|
||||
'@radix-ui/react-toast@1.2.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
|
||||
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
64
src/App.css
64
src/App.css
@@ -67,51 +67,51 @@
|
||||
}
|
||||
} */
|
||||
|
||||
/* *=========== Green theme =========== */
|
||||
/* *=========== Blue theme =========== */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 142.1 76.2% 36.3%;
|
||||
--primary-foreground: 355.7 100% 97.3%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 220 13% 91%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 142.1 76.2% 36.3%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 217 91% 60%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 0 0% 95%;
|
||||
--card: 24 9.8% 10%;
|
||||
--card-foreground: 0 0% 95%;
|
||||
--popover: 0 0% 9%;
|
||||
--popover-foreground: 0 0% 95%;
|
||||
--primary: 142.1 70.6% 45.3%;
|
||||
--primary-foreground: 144.9 80.4% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 15%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217 91% 70%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 142.4 71.8% 29.2%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 217 91% 70%;
|
||||
}
|
||||
}
|
||||
|
||||
|
31
src/App.tsx
31
src/App.tsx
@@ -1,40 +1,47 @@
|
||||
import { About } from "./components/About";
|
||||
import { Cta } from "./components/Cta";
|
||||
// import { Cta } from "./components/Cta";
|
||||
import { FAQ } from "./components/FAQ";
|
||||
import { Features } from "./components/Features";
|
||||
import { Footer } from "./components/Footer";
|
||||
import { Hero } from "./components/Hero";
|
||||
import { HowItWorks } from "./components/HowItWorks";
|
||||
// import { HowItWorks } from "./components/HowItWorks";
|
||||
import { Navbar } from "./components/Navbar";
|
||||
import { Newsletter } from "./components/Newsletter";
|
||||
import { Pricing } from "./components/Pricing";
|
||||
// import { Newsletter } from "./components/Newsletter";
|
||||
// import { Pricing } from "./components/Pricing";
|
||||
import { ScrollToTop } from "./components/ScrollToTop";
|
||||
import { Services } from "./components/Services";
|
||||
// import { Services } from "./components/Services";
|
||||
// import { Sponsors } from "./components/Sponsors";
|
||||
import { Team } from "./components/Team";
|
||||
// import { Team } from "./components/Team";
|
||||
import { Testimonials } from "./components/Testimonials";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider
|
||||
defaultTheme="dark"
|
||||
storageKey="vite-ui-theme"
|
||||
>
|
||||
<Navbar />
|
||||
<Hero />
|
||||
<Testimonials />
|
||||
<Features />
|
||||
<Testimonials />
|
||||
|
||||
{/* <Sponsors /> */}
|
||||
<FAQ />
|
||||
<About />
|
||||
<HowItWorks />
|
||||
|
||||
{/* <HowItWorks />
|
||||
<Services />
|
||||
<Cta />
|
||||
|
||||
<Team />
|
||||
<Pricing />
|
||||
<Newsletter />
|
||||
<FAQ />
|
||||
<Newsletter /> */}
|
||||
{/* <FAQ /> */}
|
||||
<Footer />
|
||||
<ScrollToTop />
|
||||
</>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,33 +1,35 @@
|
||||
import { Statistics } from "./Statistics";
|
||||
import pilot from "../assets/pilot.png";
|
||||
import { AnimatedGridPattern } from "@/components/ui/animated-grid-pattern";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const About = () => {
|
||||
return (
|
||||
<section
|
||||
id="about"
|
||||
className="container py-24 sm:py-32"
|
||||
className="container py-24 sm:py-32 relative overflow-hidden"
|
||||
>
|
||||
<div className="bg-muted/50 border rounded-lg py-12">
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(1000px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12"
|
||||
)}
|
||||
/>
|
||||
<div className="bg-muted/50 border rounded-lg py-12 relative z-10">
|
||||
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
|
||||
<img
|
||||
src={pilot}
|
||||
alt=""
|
||||
className="w-[300px] object-contain rounded-lg"
|
||||
/>
|
||||
<div className="bg-green-0 flex flex-col justify-between">
|
||||
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="pb-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">
|
||||
<span className="bg-gradient-to-b from-primary/60 to-primary text-transparent bg-clip-text">
|
||||
About{" "}
|
||||
关于稷维科技
|
||||
</span>
|
||||
Company
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mt-4">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit
|
||||
amet, consectetur adipiscing elit.
|
||||
稷维科技专注于为企业客户提供IT运维、网站与小程序开发、AI赋能、技术咨询等一站式数字化服务。我们拥有专业的技术团队,致力于用创新和高效的解决方案,助力企业数字化转型与业务升级。服务客户遍及多个行业,深受信赖。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
98
src/components/ContactModal.tsx
Normal file
98
src/components/ContactModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { FaPhoneVolume } from "react-icons/fa6";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
|
||||
const WECHAT_ID = "JIWEI-Tech";
|
||||
|
||||
export const ContactModal = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(WECHAT_ID).then(
|
||||
() => {
|
||||
setIsOpen(false); // Close the modal on copy
|
||||
toast({
|
||||
title: "复制成功",
|
||||
description: "微信号已复制到剪贴板",
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
console.error("Async: Could not copy text: ", err);
|
||||
toast({
|
||||
title: "复制失败",
|
||||
description: "请手动复制微信号。",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
className={`font-light cursor-pointer ${buttonVariants({
|
||||
variant: "ghost",
|
||||
})}`}
|
||||
>
|
||||
<FaPhoneVolume className="mr-2 w-5 h-5" />
|
||||
联系我们
|
||||
</a>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-white dark:bg-[#232c3b] rounded-2xl p-6 max-w-md w-full mx-22 shadow-2xl">
|
||||
<DialogHeader className="flex justify-between items-center mb-6 text-left">
|
||||
<DialogTitle className=" text-xl font-bold text-[#181f2a] dark:text-white">
|
||||
联系我们
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="bg-[#f3f4f6] dark:bg-[#1e293b] p-4 rounded-xl mb-4">
|
||||
<img
|
||||
src="https://lijue-me.oss-cn-chengdu.aliyuncs.com/20250617153505991.png"
|
||||
alt="联系二维码"
|
||||
className="max-w-48 max-h-60 mx-auto rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[#4b5563] dark:text-[#cbd5e1] mb-2">
|
||||
扫描二维码添加微信
|
||||
</p>
|
||||
<p className="text-sm text-[#6b7280] dark:text-[#94a3b8]">
|
||||
专业顾问为您提供一对一咨询服务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-6 flex gap-3">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1 px-4 py-2 border border-[#d1d5db] dark:border-[#374151] text-[#374151] dark:text-[#cbd5e1] rounded-lg hover:bg-[#f9fafb] dark:hover:bg-[#374151] transition-colors bg-white dark:bg-[#232c3b]"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={handleCopy}
|
||||
className="flex-1 px-4 py-2 bg-[#2563eb] text-white rounded-lg hover:bg-[#1d4ed8] transition-colors"
|
||||
>
|
||||
复制微信号
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@@ -4,6 +4,8 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { AnimatedGridPattern } from "@/components/ui/animated-grid-pattern";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FAQProps {
|
||||
question: string;
|
||||
@@ -13,33 +15,28 @@ interface FAQProps {
|
||||
|
||||
const FAQList: FAQProps[] = [
|
||||
{
|
||||
question: "Is this template free?",
|
||||
answer: "Yes. It is a free ChadcnUI template.",
|
||||
question: "稷维科技提供哪些IT服务?",
|
||||
answer: "我们为企业提供IT运维、网站/小程序开发、AI赋能、技术咨询等一站式服务。",
|
||||
value: "item-1",
|
||||
},
|
||||
{
|
||||
question: "Lorem ipsum dolor sit amet consectetur adipisicing elit?",
|
||||
answer:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sint labore quidem quam? Consectetur sapiente iste rerum reiciendis animi nihil nostrum sit quo, modi quod.",
|
||||
question: "如何联系稷维科技获取技术支持?",
|
||||
answer: "您可以通过页面下方的“联系我们”按钮,或扫描首页二维码添加微信,获得一对一技术支持。",
|
||||
value: "item-2",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Lorem ipsum dolor sit amet Consectetur natus dolores minus quibusdam?",
|
||||
answer:
|
||||
"Lorem ipsum dolor sit amet consectetur, adipisicing elit. Labore qui nostrum reiciendis veritatis necessitatibus maxime quis ipsa vitae cumque quo?",
|
||||
question: "是否支持定制开发和长期合作?",
|
||||
answer: "支持。我们为企业客户提供定制化开发和长期技术运维服务,欢迎洽谈合作。",
|
||||
value: "item-3",
|
||||
},
|
||||
{
|
||||
question: "Lorem ipsum dolor sit amet, consectetur adipisicing elit?",
|
||||
answer: "Lorem ipsum dolor sit amet consectetur, adipisicing elit.",
|
||||
question: "项目开发周期一般多久?",
|
||||
answer: "根据项目复杂度,一般为1-4周,具体可与顾问沟通确认。",
|
||||
value: "item-4",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur natus?",
|
||||
answer:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sint labore quidem quam? Consectetur sapiente iste rerum reiciendis animi nihil nostrum sit quo, modi quod.",
|
||||
question: "售后服务和保障有哪些?",
|
||||
answer: "我们提供7x24小时技术支持,定期巡检,故障快速响应,保障您的业务稳定运行。",
|
||||
value: "item-5",
|
||||
},
|
||||
];
|
||||
@@ -48,44 +45,53 @@ export const FAQ = () => {
|
||||
return (
|
||||
<section
|
||||
id="faq"
|
||||
className="container py-24 sm:py-32"
|
||||
className="container py-24 sm:py-32 relative overflow-hidden"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Frequently Asked{" "}
|
||||
<span className="bg-gradient-to-b from-primary/60 to-primary text-transparent bg-clip-text">
|
||||
Questions
|
||||
</span>
|
||||
</h2>
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(1000px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12"
|
||||
)}
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
常见问题 <span className="bg-gradient-to-b from-primary/60 to-primary text-transparent bg-clip-text">FAQ</span>
|
||||
</h2>
|
||||
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full AccordionRoot"
|
||||
>
|
||||
{FAQList.map(({ question, answer, value }: FAQProps) => (
|
||||
<AccordionItem
|
||||
key={value}
|
||||
value={value}
|
||||
>
|
||||
<AccordionTrigger className="text-left">
|
||||
{question}
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent>{answer}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<h3 className="font-medium mt-4">
|
||||
Still have questions?{" "}
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="text-primary transition-all border-primary hover:border-b-2"
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full AccordionRoot"
|
||||
>
|
||||
Contact us
|
||||
</a>
|
||||
</h3>
|
||||
{FAQList.map(({ question, answer, value }: FAQProps) => (
|
||||
<AccordionItem
|
||||
key={value}
|
||||
value={value}
|
||||
>
|
||||
<AccordionTrigger className="text-left">
|
||||
{question}
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent>{answer}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<h3 className="font-medium mt-4">
|
||||
还有其他疑问?
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="text-primary transition-all border-primary hover:border-b-2"
|
||||
>
|
||||
联系我们
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@@ -83,13 +83,16 @@ export const Features = () => {
|
||||
)}
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-xl lg:text-2xl font-bold md:text-center">
|
||||
专业的IT技术服务商 <br />
|
||||
<span className="text-sm bg-clip-text">
|
||||
我们专注于为企业提供全方位的IT技术维护、小程序开发、网站制作等服务, <br />
|
||||
助力您的业务数字化转型,提升竞争优势。
|
||||
</span>
|
||||
</h2>
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl lg:text-2xl font-bold">
|
||||
专业的IT技术服务商
|
||||
</h2>
|
||||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
我们专注于为企业提供全方位的IT技术维护、小程序开发、网站制作等服务,{" "}
|
||||
<br />
|
||||
助力您的业务数字化转型,提升竞争优势。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-wrap md:justify-center gap-4">
|
||||
{featureList.map((feature: string) => (
|
||||
@@ -108,8 +111,7 @@ export const Features = () => {
|
||||
{features.map(({ title, description, image }: FeatureProps) => (
|
||||
<div
|
||||
key={title}
|
||||
className="group relative overflow-hidden rounded-2xl border-2 border-primary/20 hover:border-primary/80 bg-gradient-to-br
|
||||
from-white/75 to-white/50 transition-all duration-500 hover:scale-105 hover:shadow-2xl hover:shadow-primary/20 aspect-[5/6]"
|
||||
className="group relative overflow-hidden rounded-2xl border-2 border-primary/20 hover:border-primary/80 bg-gradient-to-br from-background/90 to-muted/20 transition-all duration-500 hover:scale-105 hover:shadow-2xl hover:shadow-primary/20 aspect-[5/6]"
|
||||
>
|
||||
{/* 渐变边框效果 */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/40 via-primary/15 to-primary/40 opacity-0 transition-opacity duration-500 group-hover:opacity-70" />
|
||||
|
@@ -10,10 +10,10 @@ export const Footer = () => {
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="/"
|
||||
className="font-bold text-xl flex"
|
||||
className="font-bold text-xl flex items-center gap-2"
|
||||
>
|
||||
<LogoIcon />
|
||||
ShadcnUI/React
|
||||
稷维科技
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -25,40 +25,29 @@ export const Footer = () => {
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
Github
|
||||
公众号
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
Twitter
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
Dribbble
|
||||
BLOG
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-bold text-lg">平台</h3>
|
||||
<h3 className="font-bold text-lg">动态</h3>
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
网页端
|
||||
Bilibili
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -68,17 +57,7 @@ export const Footer = () => {
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
移动端
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
桌面端
|
||||
抖音
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,54 +94,17 @@ export const Footer = () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-bold text-lg">动态</h3>
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
Youtube
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="#"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
Twitch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="container pb-14 text-center">
|
||||
<h3>
|
||||
© 2025 本落地页由{" "}
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
href="https://www.linkedin.com/in/leopoldo-miranda/"
|
||||
className="text-primary transition-all border-primary hover:border-b-2"
|
||||
>
|
||||
Leo Miranda
|
||||
</a>
|
||||
制作
|
||||
</h3>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h3 className="text-sm text-muted-foreground">
|
||||
© 2025 稷维科技. 保留所有权利
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
陕ICP备2021012926号
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
);
|
||||
|
@@ -13,10 +13,10 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { FaPhoneVolume } from "react-icons/fa6";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
import { Menu } from "lucide-react";
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
import { ContactModal } from "./ContactModal";
|
||||
|
||||
interface RouteProps {
|
||||
href: string;
|
||||
@@ -150,15 +150,7 @@ export const Navbar = () => {
|
||||
</nav>
|
||||
|
||||
<div className="hidden md:flex gap-4">
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/leoMirandaa/shadcn-landing-page.git"
|
||||
target="_blank"
|
||||
className={`font-light ${buttonVariants({ variant: "ghost" })}`}
|
||||
>
|
||||
<FaPhoneVolume className="mr-2 w-5 h-5" />
|
||||
联系我们
|
||||
</a>
|
||||
<ContactModal />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</NavigationMenuList>
|
||||
|
@@ -90,7 +90,6 @@ export const Testimonials = () => {
|
||||
{/* 主标题 */}
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl lg:text-3xl font-bold mb-4">
|
||||
稷维科技:
|
||||
<span className="bg-gradient-to-r from-primary/80 to-primary text-transparent bg-clip-text">
|
||||
专业可靠的IT技术服务商
|
||||
</span>
|
||||
|
@@ -12,7 +12,7 @@ const AccordionItem = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
className={cn("", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
122
src/components/ui/dialog.tsx
Normal file
122
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
129
src/components/ui/toast.tsx
Normal file
129
src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-translation)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
171
src/components/ui/use-toast.ts
Normal file
171
src/components/ui/use-toast.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: "ADD_TOAST"
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: "UPDATE_TOAST"
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: "DISMISS_TOAST"
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: "REMOVE_TOAST"
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface Toast extends Omit<ToasterToast, "id"> {}
|
||||
|
||||
function toast(props: Toast) {
|
||||
const id = Math.random().toString(36).slice(2, 9)
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open: boolean) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast, type ToastProps }
|
Reference in New Issue
Block a user