diff --git a/package.json b/package.json index d2615bb..24c2742 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f063581..742cf54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/App.css b/src/App.css index 1eaa180..33893f0 100644 --- a/src/App.css +++ b/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%; } } diff --git a/src/App.tsx b/src/App.tsx index f0d565f..1b3067b 100644 --- a/src/App.tsx +++ b/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 ( - <> + - + + {/* */} + - + + {/* - - + */} + {/* */}
- + ); } diff --git a/src/components/About.tsx b/src/components/About.tsx index b56821c..060cb60 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -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 (
-
+ +
- -
+ +

- About{" "} + 关于稷维科技 - Company

- 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赋能、技术咨询等一站式数字化服务。我们拥有专业的技术团队,致力于用创新和高效的解决方案,助力企业数字化转型与业务升级。服务客户遍及多个行业,深受信赖。

diff --git a/src/components/ContactModal.tsx b/src/components/ContactModal.tsx new file mode 100644 index 0000000..99aa0ee --- /dev/null +++ b/src/components/ContactModal.tsx @@ -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 ( + + + + + 联系我们 + + + + + + 联系我们 + + + +
+
+ 联系二维码 +
+

+ 扫描二维码添加微信 +

+

+ 专业顾问为您提供一对一咨询服务 +

+
+ + + + + + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/FAQ.tsx b/src/components/FAQ.tsx index 0b3a43e..3c60e9f 100644 --- a/src/components/FAQ.tsx +++ b/src/components/FAQ.tsx @@ -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 (
-

- Frequently Asked{" "} - - Questions - -

+ +
+

+ 常见问题 FAQ +

- - {FAQList.map(({ question, answer, value }: FAQProps) => ( - - - {question} - - - {answer} - - ))} - - -

- Still have questions?{" "} - - Contact us - -

+ {FAQList.map(({ question, answer, value }: FAQProps) => ( + + + {question} + + + {answer} + + ))} + + +

+ 还有其他疑问? + + 联系我们 + +

+
); }; diff --git a/src/components/Features.tsx b/src/components/Features.tsx index 0b62192..8b9e43f 100644 --- a/src/components/Features.tsx +++ b/src/components/Features.tsx @@ -83,13 +83,16 @@ export const Features = () => { )} />
-

- 专业的IT技术服务商
- - 我们专注于为企业提供全方位的IT技术维护、小程序开发、网站制作等服务,
- 助力您的业务数字化转型,提升竞争优势。 -
-

+
+

+ 专业的IT技术服务商 +

+

+ 我们专注于为企业提供全方位的IT技术维护、小程序开发、网站制作等服务,{" "} +
+ 助力您的业务数字化转型,提升竞争优势。 +

+
{/*
{featureList.map((feature: string) => ( @@ -108,8 +111,7 @@ export const Features = () => { {features.map(({ title, description, image }: FeatureProps) => (
{/* 渐变边框效果 */}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 9889d7c..984633a 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -10,10 +10,10 @@ export const Footer = () => { - ShadcnUI/React + 稷维科技
@@ -25,40 +25,29 @@ export const Footer = () => { href="#" className="opacity-60 hover:opacity-100" > - Github + 公众号
- - -
-

平台

+

动态

@@ -68,17 +57,7 @@ export const Footer = () => { href="#" className="opacity-60 hover:opacity-100" > - 移动端 - -
- -
@@ -115,54 +94,17 @@ export const Footer = () => {
- -
-

动态

- - - - - -
-

- © 2025 本落地页由{" "} - - Leo Miranda - - 制作 -

+
+

+ © 2025 稷维科技. 保留所有权利 +

+

+ 陕ICP备2021012926号 +

+
); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c55f6a6..3aa52d1 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -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 = () => { diff --git a/src/components/Testimonials.tsx b/src/components/Testimonials.tsx index f1cfb3b..d7ff1d1 100644 --- a/src/components/Testimonials.tsx +++ b/src/components/Testimonials.tsx @@ -90,7 +90,6 @@ export const Testimonials = () => { {/* 主标题 */}

- 稷维科技: 专业可靠的IT技术服务商 diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 8bc66bf..6021841 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -12,7 +12,7 @@ const AccordionItem = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..766aada --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} \ No newline at end of file diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..5aebbdf --- /dev/null +++ b/src/components/ui/toast.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +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, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ No newline at end of file diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts new file mode 100644 index 0000000..320fd79 --- /dev/null +++ b/src/components/ui/use-toast.ts @@ -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>() + +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 + } + | { + type: "DISMISS_TOAST" + toastId?: ToasterToast["id"] + } + | { + type: "REMOVE_TOAST" + toastId?: ToasterToast["id"] + } + +interface Toast extends Omit {} + +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(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 } \ No newline at end of file