diff --git a/components/footer.module.css b/components/footer.module.css index f29903aee..b67e3de20 100644 --- a/components/footer.module.css +++ b/components/footer.module.css @@ -174,10 +174,11 @@ padding: 8px 12px; justify-content: center; align-items: center; - gap: 8px; + gap: 2px; border-radius: 8px; background: #5f38fb; color: #ffff; + align-items: center; } .signBtn:hover { border-radius: 8px; @@ -205,3 +206,7 @@ background: transparent; border: 1px solid var(--Dark-Mode-White-20, #4d4f57); } + +.textSuccess { + color: #17984b !important; +} diff --git a/components/footer.tsx b/components/footer.tsx index 31767295c..fd0c9482f 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, useState } from "react"; +import React, { ChangeEvent, useState, useCallback } from "react"; import styles from "./footer.module.css"; import { Anchor } from "theme/fetch-ai-docs/components"; import { renderComponent } from "theme/fetch-ai-docs/utils"; @@ -15,31 +15,21 @@ import { import Logo from "./logo"; const FooterLink = ({ - content, + content: { description, path }, }: { - content: { - description: string; - path: string; - }; + content: { description: string; path: string }; }) => { - const [hover, setHover] = useState(false); + const [isHovered, setIsHovered] = useState(false); + return (
{ - window.open(content.path, "_blank", "noopener, noreferrer"); - }} - onMouseOver={() => { - setHover(true); - }} - onMouseLeave={() => { - setHover(false); - }} + id={addUnderscoreInText(description)} + onClick={() => window.open(path, "_blank", "noopener, noreferrer")} + onMouseOver={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} > -

- {content.description} +

+ {description}

); @@ -47,58 +37,73 @@ const FooterLink = ({ const Footer: React.FC = () => { const developers = [ - { - description: "Main website", - path: "https://fetch.ai/", - }, - { - description: "Integrations", - path: "https://fetch.ai/integrations", - }, - { - description: "Events", - path: "https://fetch.ai/events", - }, - { - description: "We’re hiring!", - path: "https://fetch.ai/careers", - }, + { description: "Main website", path: "https://fetch.ai/" }, + { description: "Integrations", path: "https://fetch.ai/integrations" }, + { description: "Events", path: "https://fetch.ai/events" }, + { description: "We’re hiring!", path: "https://fetch.ai/careers" }, ]; - const [email, setEmail] = useState(""); - const [message, setMessage] = useState(""); - - const onClick = async () => { - if (email === "") { - setMessage("Please enter your email address."); - return; - } else if (!regex.test(email)) { - setMessage("Please enter a valid email address."); - return; - } + const [formData, setFormData] = useState({ + email: "", + message: "", + successMsg: "", + loading: false, + }); - const option = { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(email), - }; - await fetch("/docs/api/newsletter", option); + const handleInputChange = (event: ChangeEvent) => { + setFormData((prev) => ({ ...prev, email: event.target.value })); }; - const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const handleBlur = () => { - if (email === "") { - setMessage("Please enter your email address."); - } else if (regex.test(email)) { - setMessage(""); - } else { - setMessage("Please enter a valid email address."); + const validateEmail = useCallback(() => { + const { email } = formData; + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!email) { + setFormData((prev) => ({ + ...prev, + message: "Please enter your email address.", + })); + return false; + } + if (!regex.test(email)) { + setFormData((prev) => ({ + ...prev, + message: "Please enter a valid email address.", + })); + return false; } + return true; + }, [formData.email]); + + const onBlur = useCallback(() => { + validateEmail(); setTimeout(() => { - setMessage(""); + setFormData((prev) => ({ ...prev, message: "" })); }, 5000); + }, [validateEmail]); + + const handleSubmit = async () => { + if (!validateEmail()) return; + + setFormData((prev) => ({ ...prev, loading: true })); + + const response = await fetch("/docs/api/newsletter", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: formData.email }), + }); + + if (response.ok) { + setFormData({ + email: "", + message: "", + successMsg: "Thank you for signing up for developer updates!", + loading: false, + }); + setTimeout( + () => setFormData((prev) => ({ ...prev, successMsg: "" })), + 5000, + ); + } }; return ( @@ -109,9 +114,9 @@ const Footer: React.FC = () => {
- {developers.map((content, index) => { - return ; - })} + {developers.map((content, index) => ( + + ))}
@@ -142,7 +147,6 @@ const Footer: React.FC = () => { , )} - { , )} - { {renderComponent( <> - Youtube + YouTube , )} @@ -218,20 +221,51 @@ const Footer: React.FC = () => {
) => - setEmail(event.target.value) - } - onBlur={handleBlur} + onChange={handleInputChange} + onBlur={onBlur} className={styles.inputInner} placeholder="Email address" + value={formData.email} />
- - {message} + + { + + {formData.message || formData.successMsg} + + } diff --git a/pages/api/newsletter.ts b/pages/api/newsletter.ts index f99fba1fa..44ff19be1 100644 --- a/pages/api/newsletter.ts +++ b/pages/api/newsletter.ts @@ -6,7 +6,6 @@ export default async function handler( ) { const email = req.body; try { - const url = new URL(`${process.env.NEWSLETTER_BASE_URL}/v2/subscribers`); const headers = { Authorization: `Bearer ${process.env.SENDER_TOKEN}`, "Content-Type": "application/json", @@ -14,15 +13,18 @@ export default async function handler( }; const data = { - email, + email: email, groups: ["e1rVB0"], }; - const response = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(data), - }).then((response) => response.json()); + const response = await fetch( + `${process.env.NEWSLETTER_BASE_URL}/v2/subscribers`, + { + method: "POST", + headers, + body: JSON.stringify(data), + }, + ).then((response) => response.json()); if (!response.success) { console.log("unable to subscribe user"); } diff --git a/styles/globals.css b/styles/globals.css index 74e3b5293..1f3ba9b76 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -2561,7 +2561,6 @@ span:target + .subheading-anchor:after { font-style: normal !important; font-weight: 500 !important; line-height: normal !important; - text-transform: uppercase !important; } :is(html[class~="dark"] .first-title) { @@ -4008,6 +4007,26 @@ input[type="search"]::-webkit-search-results-decoration { background: #d2d3d5; } +.nx-bg-active-menu-os { + background-color: #efebff; +} + +:is(html[class~="dark"] .dark\:nx-bg-active-menu-os) { + background: #363841; +} + +.os-icon-normal { + fill: #8a9fb8; +} + +.os-icon-active { + fill: #5f38fb; +} + +:is(html[class~="dark"] .dark\:os-icon-active) { + fill: #bfaffd; +} + :is(html[class~="dark"] .dark\:nx-bg-indigo-500) { background-color: #5f38fb; } @@ -5129,7 +5148,6 @@ div:hover > .\[div\:hover\>\&\]\:nx-opacity-100 { align-items: flex-start; border-radius: 8px; border: var(--1, 1px) solid #b9c5d4; - margin-top: 6px; } :is(html[class~="dark"] .osmenu-container-nav) { @@ -5144,9 +5162,6 @@ div:hover > .\[div\:hover\>\&\]\:nx-opacity-100 { background-color: #363841; } -:is(html[class~="dark"] .osmenu-tab-container) { - background-color: #20222c; -} .osmenu-tab-container { display: flex; width: 44px; diff --git a/theme/fetch-ai-docs/components/instant-algolia-search.tsx b/theme/fetch-ai-docs/components/instant-algolia-search.tsx index b1ef1fad9..fb93f077c 100644 --- a/theme/fetch-ai-docs/components/instant-algolia-search.tsx +++ b/theme/fetch-ai-docs/components/instant-algolia-search.tsx @@ -102,7 +102,7 @@ export const InstantAlgoliaSearch = ({ const NoResultsBoundary = ({ fallback }: { fallback: string }) => { return (
-
{`${fallback}`}
+
{`${fallback}`}
); }; diff --git a/theme/fetch-ai-docs/components/theme-switcher.tsx b/theme/fetch-ai-docs/components/theme-switcher.tsx index dc42bd021..da8bedf8c 100644 --- a/theme/fetch-ai-docs/components/theme-switcher.tsx +++ b/theme/fetch-ai-docs/components/theme-switcher.tsx @@ -1,68 +1,126 @@ import React from "react"; import { useTheme } from "next-themes"; import { z } from "zod"; +import { useState } from "react"; +import { ThemeMode } from "../helpers"; +import { motion, AnimatePresence } from "framer-motion"; + +export const themeOptionsSchema = z.strictObject({ + light: z.string(), + dark: z.string(), + system: z.string(), +}); + +const animations = { + initial: { opacity: 0, y: -10 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -10 }, + transition: { duration: 0.3 }, +}; const osmenu = [ { - name: "light", - icon: ( + name: ThemeMode.Light, + icon: (isActive: boolean, isHovered: boolean) => ( - + ), }, { - name: "dark", - icon: ( + name: ThemeMode.Dark, + icon: (isActive: boolean, isHovered: boolean) => ( - + ), }, ]; -export const themeOptionsSchema = z.strictObject({ - light: z.string(), - dark: z.string(), - system: z.string(), -}); +const ThemeOption = ({ + item, + isActive, + isHovered, + onClick, + onMouseEnter, + onMouseLeave, +}: { + item: (typeof osmenu)[0]; + isActive: boolean; + isHovered: boolean; + onClick: () => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +}) => ( +
+ {item.icon(isActive, isHovered)} +
+); export const ThemeSwitcher = () => { - const { setTheme } = useTheme(); + const { setTheme, resolvedTheme } = useTheme(); + const [hoverIndex, setHoverIndex] = useState(null); + return ( -
-
- Theme -
- {osmenu.map((item, index) => ( -
setTheme(item.name)} - className={`osmenu-tab-container nx-justify-center nx-cursor-pointer nx-items-center`} - > - {item.icon} -
- ))} + + +
+ Theme +
+ {osmenu.map((item, index) => { + const isActive = resolvedTheme === item.name; + const isHovered = hoverIndex === index; + + return ( + setTheme(item.name)} + onMouseEnter={() => setHoverIndex(index)} + onMouseLeave={() => setHoverIndex(null)} + /> + ); + })} +
-
-
+ + ); };