diff --git a/package.json b/package.json index 0959863ebe9..eba33f3d5b1 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "react-select": "5.8.0", "reading-time": "^1.5.0", "remark-gfm": "^3.0.1", + "swiper": "^11.1.10", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.0", @@ -104,6 +105,8 @@ "@types/node": "^20.4.2", "@types/react": "18.2.57", "@types/react-dom": "18.2.19", + "@types/swiper": "^6.0.0", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "autoprefixer": "^10.4.19", @@ -133,7 +136,8 @@ "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "^5.5.2", "unified": "^10.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit": "^5.0.0", + "xml2js": "^0.6.2" }, "resolutions": { "jackspeak": "2.1.1", diff --git a/public/images/0xparc-logo.svg b/public/images/0xparc-logo.svg new file mode 100644 index 00000000000..dcb9b050a1f --- /dev/null +++ b/public/images/0xparc-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/images/attestant-logo.svg b/public/images/attestant-logo.svg new file mode 100644 index 00000000000..9a6ca3dcde6 --- /dev/null +++ b/public/images/attestant-logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/events/event-placeholder.png b/public/images/events/event-placeholder.png index 4a15165452a..a94614605d5 100644 Binary files a/public/images/events/event-placeholder.png and b/public/images/events/event-placeholder.png differ diff --git a/public/images/man-and-dog-playing.png b/public/images/man-and-dog-playing.png new file mode 100644 index 00000000000..14e75d2156e Binary files /dev/null and b/public/images/man-and-dog-playing.png differ diff --git a/public/images/man-baby-woman.png b/public/images/man-baby-woman.png new file mode 100644 index 00000000000..92ce90dcd26 Binary files /dev/null and b/public/images/man-baby-woman.png differ diff --git a/public/images/panda-ops-banner.png b/public/images/panda-ops-banner.png new file mode 100644 index 00000000000..6f80160d92e Binary files /dev/null and b/public/images/panda-ops-banner.png differ diff --git a/public/images/robot-help-bar.png b/public/images/robot-help-bar.png new file mode 100644 index 00000000000..4e698736a19 Binary files /dev/null and b/public/images/robot-help-bar.png differ diff --git a/public/images/solidity-banner.png b/public/images/solidity-banner.png new file mode 100644 index 00000000000..3fd5a37c62e Binary files /dev/null and b/public/images/solidity-banner.png differ diff --git a/public/images/vitalik-blog-banner.svg b/public/images/vitalik-blog-banner.svg new file mode 100644 index 00000000000..cea992081c9 --- /dev/null +++ b/public/images/vitalik-blog-banner.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/@chakra-ui/components/Button.ts b/src/@chakra-ui/components/Button.ts index 1b51d328012..c0741641a70 100644 --- a/src/@chakra-ui/components/Button.ts +++ b/src/@chakra-ui/components/Button.ts @@ -44,16 +44,16 @@ const baseStyle = defineStyle({ }) const variantSolid = defineStyle({ - color: "background.base", - bg: "primary.base", + color: "white", + bg: "primary.action", borderColor: "transparent", _disabled: { bg: "disabled", color: "background.base", }, _hover: { - color: "background.base", - bg: "primary.hover", + color: "white", + bg: "primary.actionHover", boxShadow: "buttonHover", }, _active: { diff --git a/src/@chakra-ui/foundations/colors.ts b/src/@chakra-ui/foundations/colors.ts index dbf3e6cd57e..58f443a9282 100644 --- a/src/@chakra-ui/foundations/colors.ts +++ b/src/@chakra-ui/foundations/colors.ts @@ -2,41 +2,54 @@ export type Colors = typeof colors const colors = { gray: { - 100: "#F7F7F7", - 150: "#F2F2F2", - 200: "#E7E7E7", - 300: "#C8C8C8", + 50: "#f7f7f7", + 100: "#eeeeee", + 150: "#ececec", + 200: "#cecece", + 300: "#acacac", 400: "#8C8C8C", 500: "#616161", 600: "#333333", 700: "#222222", - 800: "#1B1B1B", - 900: "#141414", + 800: "#1b1b1b", + 900: "#121212", + 950: "#0a0a0a", }, blue: { - 50: "#F6F6FF", - 100: "#EBEBFF", - 200: "#D6D6FF", - 300: "#9999FF", - 400: "#5555FF", - 500: "#1C1CFF", - 600: "#0000E0", - 700: "#0000A3", - 800: "#000066", - 900: "#000029", + 50: "#F8FBFF", + 100: "#E8F1FF", + 200: "#CADFFB", + 300: "#88AAF1", + 400: "#6995F7", + 500: "#4473EF", + 600: "#3C4CEB", + 700: "#2B36A8", + 800: "#232F71", + 900: "#1B273A", }, orange: { 50: "#FFF3ED", - 100: "#FFE5D6", - 200: "#FFCBAD", - 300: "#FFB185", - 400: "#FF985C", - 500: "#FF7324", - 550: "#DF5A0E", - 600: "#B84300", - 700: "#7A2D00", - 800: "#521E00", - 900: "#2F1000", + 100: "#FFF0DB", + 200: "#FFD7A7", + 300: "#FEB077", + 400: "#FD8640", + 500: "#FB610E", + 600: "#EC4A0A", + 700: "#C4350A", + 800: "#7D2711", + 900: "#3A291D", + }, + purple: { + 50: "#F3ECFF", + 100: "#EDE2FF", + 200: "#DAC5FC", + 300: "#CCAFFC", + 400: "#B38DF0", + 500: "#945AF4", + 600: "#6C24DF", + 700: "#561BB5", + 800: "#41128B", + 900: "#1E0546", }, red: { 100: "#f7c8c8", diff --git a/src/@chakra-ui/semanticTokens.ts b/src/@chakra-ui/semanticTokens.ts index 9b81ae3c21a..7d3a68a0a18 100644 --- a/src/@chakra-ui/semanticTokens.ts +++ b/src/@chakra-ui/semanticTokens.ts @@ -37,11 +37,13 @@ const semanticTokens = { // Main Set primary: { - base: { _light: "blue.500", _dark: "orange.500" }, - highContrast: { _light: "blue.800", _dark: "orange.100" }, - lowContrast: { _light: "blue.100", _dark: "orange.800" }, - hover: { _light: "blue.400", _dark: "orange.400" }, - visited: { _light: "blue.700", _dark: "orange.550" }, + base: { _light: "purple.600", _dark: "purple.400" }, + highContrast: { _light: "purple.800", _dark: "purple.200" }, + lowContrast: { _light: "purple.100", _dark: "purple.900" }, + hover: { _light: "purple.500", _dark: "purple.500" }, + visited: { _light: "purple.700", _dark: "purple.300" }, + action: { _light: "purple.600", _dark: "purple.600" }, + actionHover: { _light: "purple.500", _dark: "purple.500" }, // ! Deprecating primary.light light: { _light: "blue.100", _dark: "orange.100" }, // ! Deprecating primary.dark @@ -57,7 +59,7 @@ const semanticTokens = { inverted: { _light: "gray.100", _dark: "gray.800" }, }, background: { - base: { _light: "white", _dark: "gray.800" }, + base: { _light: "white", _dark: "gray.950" }, highlight: { _light: "gray.100", _dark: "gray.900" }, }, disabled: { _light: "gray.400", _dark: "gray.500" }, @@ -91,7 +93,6 @@ const semanticTokens = { _light: "blackAlpha.400", _dark: "whiteAlpha.400", }, - switchBackground: { _light: "gray.300", _dark: "whiteAlpha.400" }, hubHeroContentBg: { _light: "rgba(255, 255, 255, 0.80)", _dark: "rgba(34, 34, 34, 0.80)", diff --git a/src/components/BigNumber/index.tsx b/src/components/BigNumber/index.tsx new file mode 100644 index 00000000000..d276611edd4 --- /dev/null +++ b/src/components/BigNumber/index.tsx @@ -0,0 +1,76 @@ +import { type ReactNode } from "react" +import { useRouter } from "next/router" +import { useTranslation } from "next-i18next" +import { MdInfoOutline } from "react-icons/md" + +import { cn } from "@/lib/utils/cn" +import { isValidDate } from "@/lib/utils/date" + +import Tooltip from "../Tooltip" +import Link from "../ui/Link" + +type BigNumberProps = { + children: ReactNode + value?: ReactNode + sourceName?: string + sourceUrl?: string + lastUpdated?: number | string + className?: string +} + +const BigNumber = ({ + children, + value, + sourceName, + sourceUrl, + lastUpdated, + className, +}: BigNumberProps) => { + const { t } = useTranslation("common") + const { locale } = useRouter() + const lastUpdatedDisplay = + lastUpdated && isValidDate(lastUpdated) + ? new Intl.DateTimeFormat(locale, { + dateStyle: "medium", + }).format(new Date(lastUpdated)) + : "" + return ( +
+ {value ? ( + <> +
{value}
+
+ {children} + {sourceName && sourceUrl && ( + +

+ {t("data-provided-by")}{" "} + {sourceName} +

+ {lastUpdated && ( +

+ {t("last-updated")}: {lastUpdatedDisplay} +

+ )} + + } + > + +
+ )} +
+ + ) : ( + {t("loading-error-refresh")} + )} +
+ ) +} +export default BigNumber diff --git a/src/components/Buttons/SvgButtonLink.tsx b/src/components/Buttons/SvgButtonLink.tsx new file mode 100644 index 00000000000..a070a9e30dc --- /dev/null +++ b/src/components/Buttons/SvgButtonLink.tsx @@ -0,0 +1,73 @@ +import { cva, VariantProps } from "class-variance-authority" +import type { FC, ReactNode, SVGProps } from "react" + +import { cn } from "@/lib/utils/cn" + +import { BaseLink } from "../ui/Link" + +type SvgButtonLinkProps = { + Svg: FC> + label?: string + children: ReactNode + href: string + className?: string + size?: number +} + +const variants = cva("flex items-center gap-3.5", { + variants: { + variant: { + col: "flex-col text-center [&_.body]:text-center", + row: "flex-row text-start [&_.body]:text-start [&_.header]:self-start", + }, + }, + defaultVariants: { + variant: "row", + }, +}) + +type Variants = VariantProps + +const SvgButtonLink = ({ + label, + children, + Svg, + className, + variant, + ...props +}: SvgButtonLinkProps & Variants) => ( + +
+
+ +
+
+ {label &&

{label}

} + {children} +
+
+
+) + +export default SvgButtonLink diff --git a/src/components/Chevron/index.tsx b/src/components/Chevron/index.tsx new file mode 100644 index 00000000000..33116e5cdd9 --- /dev/null +++ b/src/components/Chevron/index.tsx @@ -0,0 +1,21 @@ +import { MdChevronLeft, MdChevronRight } from "react-icons/md" + +import { cn } from "@/lib/utils/cn" + +import { useRtlFlip } from "@/hooks/useRtlFlip" + +export const ChevronNext = ({ + className, + ...props +}: React.HTMLAttributes) => { + const { twFlipForRtl } = useRtlFlip() + return +} + +export const ChevronPrev = ({ + className, + ...props +}: React.HTMLAttributes) => { + const { twFlipForRtl } = useRtlFlip() + return +} diff --git a/src/components/Codeblock.tsx b/src/components/Codeblock.tsx index 9f00303918c..ead9c3ddb85 100644 --- a/src/components/Codeblock.tsx +++ b/src/components/Codeblock.tsx @@ -6,34 +6,32 @@ import Highlight, { PrismTheme, } from "prism-react-renderer" import Prism from "prism-react-renderer/prism" -import { Box, BoxProps, Flex, useColorModeValue } from "@chakra-ui/react" // https://github.com/FormidableLabs/prism-react-renderer/tree/master#custom-language-support import CopyToClipboard from "@/components/CopyToClipboard" import Emoji from "@/components/Emoji" +import { Flex } from "@/components/ui/flex" + +import { cn } from "@/lib/utils/cn" import { LINES_BEFORE_COLLAPSABLE } from "@/lib/constants" + +import useColorModeValue from "@/hooks/useColorModeValue" ;(typeof global !== "undefined" ? global : window).Prism = Prism require("prismjs/components/prism-solidity") -const TopBarItem = (props: BoxProps) => { - const bgColor = useColorModeValue("#f7f7f7", "#363641") - +const TopBarItem = ({ + className, + ...props +}: React.HTMLAttributes) => { return ( - ) @@ -42,8 +40,8 @@ const TopBarItem = (props: BoxProps) => { const codeTheme = { light: { plain: { - backgroundColor: "#fafafa", - color: "#333333", + backgroundColor: "#f7f7f7", // background-highlight (gray-50) + color: "#6C24DF", // primary (purple-600) }, styles: [ { @@ -114,8 +112,8 @@ const codeTheme = { dark: { // Pulled from `defaultProps.theme` for potential customization plain: { - backgroundColor: "#2a2734", - color: "#9a86fd", + backgroundColor: "#121212", // background-highlight (gray-900) + color: "#B38DF0", // primary (purple-400) }, styles: [ { @@ -251,18 +249,17 @@ const Codeblock = ({ return ( /* Overwrites codeblocks inheriting RTL styling in Right-To-Left script languages (e.g. Arabic) */ /* Context: https://github.com/ethereum/ethereum-org-website/issues/6202 */ - - +
{({ className, style, tokens, getLineProps, getTokenProps }) => ( - {tokens.map((line, i) => { return i === tokens.length - 1 && line[0].content === "" ? null : ( - {shouldShowLineNumbers && ( - + {i + 1} - + )} - + {line.map((token, key) => ( ))} - - + +
) })} {!fromHomepage && ( {allowCollapse && totalLines - 1 > LINES_BEFORE_COLLAPSABLE && ( @@ -349,11 +331,11 @@ const Codeblock = ({ )} )} -
+ )} -
-
+ + ) } diff --git a/src/components/CommunityEvents/index.tsx b/src/components/CommunityEvents/index.tsx deleted file mode 100644 index b963a8bb03d..00000000000 --- a/src/components/CommunityEvents/index.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useRouter } from "next/router" -import { useTranslation } from "next-i18next" -import { FaDiscord } from "react-icons/fa" -import { - Box, - Center, - Divider, - Flex, - Grid, - GridItem, - Icon, -} from "@chakra-ui/react" - -import type { Lang } from "@/lib/types" -import type { CommunityEvent } from "@/lib/interfaces" - -import { ButtonLink } from "@/components/Buttons" -import InlineLink from "@/components/Link" -import OldHeading from "@/components/OldHeading" -import Text from "@/components/OldText" -import Translation from "@/components/Translation" - -import { trackCustomEvent } from "@/lib/utils/matomo" -import { getLocaleTimestamp } from "@/lib/utils/time" - -const matomoEvent = (buttonType: string) => { - trackCustomEvent({ - eventCategory: "CommunityEventsWidget", - eventAction: "clicked", - eventName: buttonType, - }) -} - -type EventProps = { - event: CommunityEvent - type: "upcoming" | "past" -} - -const Event = ({ event, type }: EventProps) => { - const { locale } = useRouter() - const { date, title, calendarLink } = event - const options: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - } - - return ( - - - - {getLocaleTimestamp(locale! as Lang, date, options)} - - - - matomoEvent(type)}> - {title} - - - - ) -} - -type CommunityEventsProps = { - events: { - pastEventData: CommunityEvent[] - upcomingEventData: CommunityEvent[] - } -} - -const CommunityEvents = ({ events }: CommunityEventsProps) => { - const { locale } = useRouter() - const { t } = useTranslation("page-index") - const { pastEventData, upcomingEventData } = events - - const reversedUpcomingEventData = upcomingEventData.slice().reverse() - const reversedPastEventData = pastEventData.slice().reverse() - - return ( - -
- - - {t("page-index:community-events-content-heading")} - - - - - {t("page-index:community-events-content-2")} - -
- - - - {reversedUpcomingEventData.length ? ( - - - {reversedUpcomingEventData[0].title} - - - {getLocaleTimestamp( - locale! as Lang, - reversedUpcomingEventData[0].date, - { - year: "numeric", - month: "long", - day: "numeric", - hour12: false, - hour: "numeric", - minute: "numeric", - } - )} - - - ({Intl.DateTimeFormat().resolvedOptions().timeZone}) - - - ) : ( - - {t("page-index:community-events-no-events-planned")} - - )} - - matomoEvent("discord")} - > - - Join Discord - - {reversedUpcomingEventData[0] && ( - matomoEvent("Add to calendar")} - fontWeight={700} - > - {t("community-events-add-to-calendar")} - - )} - - - - - - {t("page-index:community-events-upcoming-calls")} - - - {reversedUpcomingEventData.slice(1).length ? ( - reversedUpcomingEventData.slice(1).map((item, idx) => { - return - }) - ) : ( - - {t("page-index:community-events-no-upcoming-calls")} - - )} - - {t("page-index:community-events-previous-calls")} - - - {reversedPastEventData.length ? ( - reversedPastEventData.map((item, idx) => { - return - }) - ) : ( - - {t("page-index:community-events-there-are-no-past-calls")} - - )} - - -
- ) -} - -export default CommunityEvents diff --git a/src/components/EventCard.tsx b/src/components/EventCard.tsx index db577c14eee..fea404ed71d 100644 --- a/src/components/EventCard.tsx +++ b/src/components/EventCard.tsx @@ -3,11 +3,15 @@ import { useRouter } from "next/router" import { useTranslation } from "next-i18next" import { BsCalendar3 } from "react-icons/bs" import { Box, Flex, Heading, Icon } from "@chakra-ui/react" -import { Image } from "@chakra-ui/react" + +import type { EventCardProps } from "@/lib/types" import { ButtonLink } from "./Buttons" +import { TwImage } from "./Image" import Text from "./OldText" +import EventFallback from "@/public/images/events/event-placeholder.png" + const clearStyles = { content: '""', display: "block", @@ -15,18 +19,6 @@ const clearStyles = { clear: "both", } -export type EventCardProps = { - title: string - href: string - date: string - startDate: string - endDate: string - description: string - className?: string - location: string - imageUrl?: string -} - const EventCard = ({ title, href, @@ -90,14 +82,16 @@ const EventCard = ({ justifyContent="center" boxShadow="rgb(0 0 0 / 10%) 0px -1px 0px inset;" > - {title} + {imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {title} + ) : ( + + )} diff --git a/src/components/Hero/HomeHero/index.tsx b/src/components/Hero/HomeHero/index.tsx index c4408f80b5e..e333649c95a 100644 --- a/src/components/Hero/HomeHero/index.tsx +++ b/src/components/Hero/HomeHero/index.tsx @@ -1,51 +1,37 @@ import { useTranslation } from "next-i18next" -import { Box, Heading, Stack, Text, VStack } from "@chakra-ui/react" -import type { CommonHeroProps } from "@/lib/types" +import type { ClassNameProp, CommonHeroProps } from "@/lib/types" -import { ButtonLink } from "@/components/Buttons" -import { Image } from "@/components/Image" +import { TwImage } from "@/components/Image" import Morpher from "@/components/Morpher" -export type HomeHeroProps = Pick +export type HomeHeroProps = Pick & ClassNameProp -const HomeHero = ({ heroImg }: HomeHeroProps) => { +const HomeHero = ({ heroImg, className }: HomeHeroProps) => { const { t } = useTranslation("page-index") + return ( - - - +
+ - - - - - - - {t("page-index:page-index-title")} - - {t("page-index:page-index-description")} - - {t("page-index:page-index-title-button")} - - - - - +
+
+ +
+

{t("page-index:page-index-title")}

+

+ {t("page-index:page-index-description")} +

+
+
+ ) } diff --git a/src/components/Homepage/BentoCard.tsx b/src/components/Homepage/BentoCard.tsx new file mode 100644 index 00000000000..1ff68bb53cb --- /dev/null +++ b/src/components/Homepage/BentoCard.tsx @@ -0,0 +1,53 @@ +import { HTMLAttributes } from "react" +import { type StaticImageData } from "next/image" + +import { TwImage } from "@/components/Image" +import { ButtonLink } from "@/components/ui/buttons/Button" + +import { cn } from "@/lib/utils/cn" + +import { ChevronNext } from "../Chevron" +import { Card, CardTitle } from "../ui/card" +import { Center } from "../ui/flex" + +export type BentoCardProps = HTMLAttributes & { + action: string + href: string + imgSrc: StaticImageData + imgWidth?: number + imgHeight?: number + title: string +} + +const BentoCard = ({ + action, + children, + className, + href, + imgSrc, + imgWidth, + imgHeight, + title, +}: BentoCardProps) => ( + +
+ +
+
+ + {title} + +

{children}

+ + {action} + +
+
+) + +export default BentoCard diff --git a/src/components/Homepage/useBentoBox.ts b/src/components/Homepage/useBentoBox.ts new file mode 100644 index 00000000000..ecfd63f7b34 --- /dev/null +++ b/src/components/Homepage/useBentoBox.ts @@ -0,0 +1,137 @@ +import { useTranslation } from "next-i18next" + +import { cn } from "@/lib/utils/cn" + +import ImpactImage from "@/public/images/impact_transparent.png" +import ManAndDogImage from "@/public/images/man-and-dog-playing.png" +import ManBabyWomanImage from "@/public/images/man-baby-woman.png" +import RobotBarImage from "@/public/images/robot-help-bar.png" +import MergeImage from "@/public/images/upgrades/merge.png" + +type Breakpoint = "mobile" | "lg" | "xl" +type Direction = "down" | "up" | "right" | "left" +type Color = "primary" | "accent-a" | "accent-b" | "accent-c" + +const gradientStops = "from-20% to-60%" + +const colorOptions: Record = { + primary: cn( + gradientStops, + "from-primary/10 to-primary/5 dark:from-primary/20 dark:to-primary/10 border-primary/10" + ), + "accent-a": cn( + gradientStops, + "from-accent-a/10 to-accent-a/5 dark:from-accent-a/20 dark:to-accent-a/10 border-accent-a/10" + ), + "accent-b": cn( + gradientStops, + "from-accent-b/10 to-accent-b/5 dark:from-accent-b/20 dark:to-accent-b/10 border-accent-b/10" + ), + "accent-c": cn( + gradientStops, + "from-accent-c/10 to-accent-c/5 dark:from-accent-c/20 dark:to-accent-c/10 border-accent-c/10" + ), +} + +const flow: Record> = { + mobile: { + down: "flex-col bg-gradient-to-b", + up: "flex-col-reverse bg-gradient-to-t", + right: "flex-row bg-gradient-to-r", + left: "flex-row-reverse bg-gradient-to-l", + }, + lg: { + down: "lg:flex-col lg:bg-gradient-to-b", + up: "lg:flex-col-reverse lg:bg-gradient-to-t", + right: "lg:flex-row lg:bg-gradient-to-r", + left: "lg:flex-row-reverse lg:bg-gradient-to-l", + }, + xl: { + down: "xl:flex-col xl:bg-gradient-to-b", + up: "xl:flex-col-reverse xl:bg-gradient-to-t", + right: "xl:flex-row xl:bg-gradient-to-r", + left: "xl:flex-row-reverse xl:bg-gradient-to-l", + }, +} + +const stylesByPosition: Record = { + mobile: [ + flow.mobile.down, + flow.mobile.down, + flow.mobile.down, + flow.mobile.down, + flow.mobile.down, + ], + lg: [ + cn("lg:col-span-6 lg:row-start-2", flow.lg.up), + cn("lg:col-span-6 lg:col-start-7 lg:row-start-2", flow.lg.down), + cn("lg:col-span-12 lg:row-start-3", flow.lg.right), + cn("lg:col-span-6 lg:col-start-7 lg:row-start-4", flow.lg.up), + cn("lg:col-span-6 lg:row-start-4", flow.lg.down), + ], + xl: [ + cn("xl:col-span-7 xl:col-start-5 xl:row-start-1", flow.xl.right), + cn("xl:col-span-4 xl:col-start-2 xl:row-start-2", flow.xl.up), + cn("xl:col-span-3 xl:col-start-6 xl:row-start-2", flow.xl.down), + cn("xl:col-span-3 xl:col-start-9 xl:row-span-2 xl:row-start-2", flow.xl.up), + cn("xl:col-span-7 xl:col-start-2 xl:row-start-3", flow.xl.right), + ], +} + +const getPosition = (position: number): string => + cn( + stylesByPosition.mobile[position], + stylesByPosition.lg[position], + stylesByPosition.xl[position] + ) + +export const useBentoBox = () => { + const { t } = useTranslation("page-index") + + return [ + { + title: t("page-index-bento-stablecoins-title"), + children: t("page-index:page-index-bento-stablecoins-content"), + action: t("page-index:page-index-bento-stablecoins-action"), + href: "/stablecoins/", + imgSrc: ImpactImage, + imgWidth: 400, + className: cn(colorOptions["primary"], getPosition(0)), + }, + { + title: t("page-index:page-index-bento-defi-title"), + children: t("page-index:page-index-bento-defi-content"), + action: t("page-index:page-index-bento-defi-action"), + href: "/defi/", + imgSrc: ManAndDogImage, + className: cn(colorOptions["accent-c"], getPosition(1)), + }, + { + title: t("page-index:page-index-bento-dapps-title"), + children: t("page-index:page-index-bento-dapps-content"), + action: t("page-index:page-index-bento-dapps-action"), + href: "/dapps/", + imgSrc: MergeImage, + imgWidth: 320, + className: cn(colorOptions["accent-b"], getPosition(2)), + }, + { + title: t("page-index:page-index-bento-networks-title"), + children: t("page-index:page-index-bento-networks-content"), + action: t("page-index:page-index-bento-networks-action"), + href: "/layer-2/", + imgSrc: ManBabyWomanImage, + imgWidth: 324, + className: cn(colorOptions["accent-a"], getPosition(3)), + }, + { + title: t("page-index:page-index-bento-assets-title"), + children: t("page-index:page-index-bento-assets-content"), + action: t("page-index:page-index-bento-assets-action"), + href: "/nft/", + imgSrc: RobotBarImage, + imgWidth: 324, + className: cn(colorOptions["primary"], getPosition(4)), + }, + ] +} diff --git a/src/components/Homepage/useHome.ts b/src/components/Homepage/useHome.ts new file mode 100644 index 00000000000..1d7993aee5f --- /dev/null +++ b/src/components/Homepage/useHome.ts @@ -0,0 +1,203 @@ +import { useState } from "react" +import { useRouter } from "next/router" +import { useTranslation } from "next-i18next" +import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6" + +import type { EventCardProps, Lang } from "@/lib/types" +import type { CodeExample } from "@/lib/interfaces" + +import { useBentoBox } from "@/components/Homepage/useBentoBox" +import BlockHeap from "@/components/icons/block-heap.svg" +import EthGlyphIcon from "@/components/icons/eth-glyph.svg" +import EthTokenIcon from "@/components/icons/eth-token.svg" +import PickWalletIcon from "@/components/icons/eth-wallet.svg" +import ChooseNetworkIcon from "@/components/icons/network-layers.svg" +import TryAppsIcon from "@/components/icons/phone-homescreen.svg" +import RoadmapSign from "@/components/icons/roadmap-sign.svg" +import Whitepaper from "@/components/icons/whitepaper.svg" + +import { isValidDate } from "@/lib/utils/date" +import { isLangRightToLeft } from "@/lib/utils/translations" + +import events from "@/data/community-events.json" +import CreateWalletContent from "@/data/CreateWallet" + +import { GITHUB_REPO_URL } from "@/lib/constants" + +import SimpleDomainRegistryContent from "!!raw-loader!@/data/SimpleDomainRegistry.sol" +import SimpleTokenContent from "!!raw-loader!@/data/SimpleToken.sol" +import SimpleWalletContent from "!!raw-loader!@/data/SimpleWallet.sol" + +export const useHome = () => { + const { t } = useTranslation(["common", "page-index"]) + const { locale, asPath } = useRouter() + + const [isModalOpen, setModalOpen] = useState(false) + const [activeCode, setActiveCode] = useState(0) + + const bentoItems = useBentoBox() + + const dir = isLangRightToLeft(locale as Lang) ? "rtl" : "ltr" + + const toggleCodeExample = (id: number): void => { + setActiveCode(id) + setModalOpen(true) + } + + const codeExamples: CodeExample[] = [ + { + title: t("page-index:page-index-developers-code-example-title-0"), + description: t( + "page-index:page-index-developers-code-example-description-0" + ), + codeLanguage: "language-solidity", + code: SimpleWalletContent, + }, + { + title: t("page-index:page-index-developers-code-example-title-1"), + description: t( + "page-index:page-index-developers-code-example-description-1" + ), + codeLanguage: "language-solidity", + code: SimpleTokenContent, + }, + { + title: t("page-index:page-index-developers-code-example-title-2"), + description: t( + "page-index:page-index-developers-code-example-description-2" + ), + codeLanguage: "language-javascript", + code: CreateWalletContent, + }, + { + title: t("page-index:page-index-developers-code-example-title-3"), + description: t( + "page-index:page-index-developers-code-example-description-3" + ), + codeLanguage: "language-solidity", + code: SimpleDomainRegistryContent, + }, + ] + + const subHeroCTAs = [ + { + label: t("page-index:page-index-cta-wallet-label"), + description: t("page-index:page-index-cta-wallet-description"), + href: "/wallets/find-wallet/", + Svg: PickWalletIcon, + className: "text-primary hover:text-primary-hover", // TODO: Confirm hover style + }, + { + label: t("page-index:page-index-cta-get-eth-label"), + description: t("page-index:page-index-cta-get-eth-description"), + href: "/get-eth/", + Svg: EthTokenIcon, + className: "text-accent-a hover:text-accent-a-hover", + }, + { + label: t("page-index:page-index-cta-networks-label"), + description: t("page-index:page-index-cta-networks-description"), + href: "/layer-2/", // TODO: Update with new networks page when ready + Svg: ChooseNetworkIcon, + className: "text-accent-b hover:text-accent-b-hover", + }, + { + label: t("page-index:page-index-cta-dapps-label"), + description: t("page-index:page-index-cta-dapps-description"), + href: "/dapps/", + Svg: TryAppsIcon, + className: "text-accent-c hover:text-accent-c-hover", + }, + ] + + const popularTopics = [ + { + label: t("page-index:page-index-popular-topics-ethereum"), + Svg: EthTokenIcon, + href: "/what-is-ethereum/", + }, + { + label: t("page-index:page-index-popular-topics-wallets"), + Svg: PickWalletIcon, + href: "/wallets/", + }, + { + label: t("page-index:page-index-popular-topics-start"), + Svg: BlockHeap, + href: "/guides/", + }, + { + label: t("page-index:page-index-popular-topics-whitepaper"), + Svg: Whitepaper, + href: "/whitepaper/", + }, + { + label: t("page-index:page-index-popular-topics-roadmap"), + Svg: RoadmapSign, + href: "/roadmap/", + }, + ] + + const upcomingEvents = events + .filter((event) => { + const isValid = isValidDate(event.endDate) + const beginningOfEndDate = new Date(event.endDate).getTime() + const endOfEndDate = beginningOfEndDate + 24 * 60 * 60 * 1000 + const isUpcoming = endOfEndDate >= new Date().getTime() + return isValid && isUpcoming + }) + .sort( + (a, b) => new Date(a.endDate).getTime() - new Date(b.endDate).getTime() + ) + .slice(0, 3) as EventCardProps[] // Show 3 events ending soonest + + const joinActions = [ + { + Svg: EthGlyphIcon, + label: t("page-index:page-index-join-action-contribute-label"), + href: "/contributing/", + className: "text-accent-c hover:text-accent-c-hover", + description: t( + "page-index:page-index-join-action-contribute-description" + ), + }, + { + Svg: FaGithub, + label: "GitHub", + href: GITHUB_REPO_URL, + className: "text-accent-a hover:text-accent-a-hover", + description: t("page-index:page-index-join-action-github-description"), + }, + { + Svg: FaDiscord, + label: "Discord", + href: "/discord/", + className: "text-primary hover:text-primary-hover", + description: t("page-index:page-index-join-action-discord-description"), + }, + { + Svg: FaXTwitter, + label: "X", + href: "https://x.com/EthDotOrg", + className: "text-accent-b hover:text-accent-b-hover", + description: t("page-index:page-index-join-action-twitter-description"), + }, + ] + + return { + t, + locale, + asPath, + dir, + isModalOpen, + setModalOpen, + activeCode, + toggleCodeExample, + codeExamples, + subHeroCTAs, + popularTopics, + upcomingEvents, + joinActions, + bentoItems, + } +} diff --git a/src/components/Morpher.tsx b/src/components/Morpher.tsx index 36e157bad76..0a8f7c91d87 100644 --- a/src/components/Morpher.tsx +++ b/src/components/Morpher.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react" -import { useBreakpointValue } from "@chakra-ui/react" +import { useMediaQuery } from "usehooks-ts" -import { Button } from "@/components/Buttons" +import { Button } from "@/components/ui/buttons/Button" import { DESKTOP_LANGUAGE_BUTTON_NAME, @@ -125,6 +125,8 @@ const Morpher = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const isLarge = useMediaQuery("(min-width: 48rem)") // TW md breakpoint, 768px + const handleMobileClick = () => { if (!document) return ;(document.getElementById(HAMBURGER_BUTTON_ID) as HTMLButtonElement).click() @@ -147,25 +149,16 @@ const Morpher = () => { ).click() } - const handleClick = - useBreakpointValue({ - base: handleMobileClick, - md: handleDesktopClick, - }) || handleDesktopClick - return ( - + <> + + ) } diff --git a/src/components/Nav/Menu/NextChevron.tsx b/src/components/Nav/Menu/NextChevron.tsx deleted file mode 100644 index 425d211b1bb..00000000000 --- a/src/components/Nav/Menu/NextChevron.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useRouter } from "next/router" -import { MdChevronLeft, MdChevronRight } from "react-icons/md" -import { Icon, type IconProps } from "@chakra-ui/react" - -import type { Lang } from "@/lib/types" - -import { isLangRightToLeft } from "@/lib/utils/translations" - -const NextChevron = (props: IconProps) => { - const { locale } = useRouter() - const isRtl = isLangRightToLeft(locale! as Lang) - return -} - -export default NextChevron diff --git a/src/components/Nav/Menu/SubMenu.tsx b/src/components/Nav/Menu/SubMenu.tsx index e53f2a0f5ae..621772c304b 100644 --- a/src/components/Nav/Menu/SubMenu.tsx +++ b/src/components/Nav/Menu/SubMenu.tsx @@ -19,6 +19,7 @@ import { } from "@radix-ui/react-navigation-menu" import { ButtonProps } from "@/components/Buttons" +import { ChevronNext } from "@/components/Chevron" import Link from "@/components/Link" import { trackCustomEvent } from "@/lib/utils/matomo" @@ -27,7 +28,6 @@ import { cleanPath } from "@/lib/utils/url" import type { Level, NavItem, NavSectionKey } from "../types" import ItemContent from "./ItemContent" -import NextChevron from "./NextChevron" import { useSubMenu } from "./useSubMenu" type LvlContentProps = { @@ -83,7 +83,7 @@ const SubMenu = ({ lvl, items, activeSection, onClose }: LvlContentProps) => { const buttonProps: ButtonProps = { color: menuColors.body, leftIcon: lvl === 1 && icon ? : undefined, - rightIcon: isLink ? undefined : , + rightIcon: isLink ? undefined : , position: "relative", w: "full", me: -PADDING, diff --git a/src/components/Nav/useNav.ts b/src/components/Nav/useNav.ts index 32759b79e9c..0b3a2c20716 100644 --- a/src/components/Nav/useNav.ts +++ b/src/components/Nav/useNav.ts @@ -1,3 +1,4 @@ +import { useEffect } from "react" import { useTranslation } from "next-i18next" import { useTheme } from "next-themes" import { @@ -26,15 +27,11 @@ import { trackCustomEvent } from "@/lib/utils/matomo" import type { NavSections } from "./types" -import useColorModeValue from "@/hooks/useColorModeValue" - export const useNav = () => { const { t } = useTranslation("common") - const { resolvedTheme, setTheme } = useTheme() + const { setTheme, resolvedTheme, systemTheme } = useTheme() const { setColorMode } = useColorMode() - const colorToggleEvent = useColorModeValue("dark mode", "light mode") // This will be inverted as the state is changing - const linkSections: NavSections = { learn: { label: t("learn"), @@ -465,13 +462,27 @@ export const useNav = () => { }, } + // Listen for changes to systemTheme and update theme accordingly + // Important if the user has not engaged the color mode toggle yet, and + // toggles system color preferences + useEffect(() => { + setTheme("system") + setColorMode(systemTheme) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [systemTheme]) + const toggleColorMode = () => { - setTheme(resolvedTheme === "dark" ? "light" : "dark") - setColorMode(resolvedTheme === "dark" ? "light" : "dark") + // resolvedTheme: "light" | "dark" = Current resolved color mode from useTheme + const targetTheme = resolvedTheme === "dark" ? "light" : "dark" + // If target theme matches the users system pref, set ls theme to "system" + const lsTheme = targetTheme === systemTheme ? "system" : targetTheme + + setTheme(lsTheme) + setColorMode(targetTheme) trackCustomEvent({ eventCategory: "nav bar", eventAction: "click", - eventName: colorToggleEvent, + eventName: `${targetTheme} mode`, }) } diff --git a/src/components/StatsBoxGrid/GridItem.tsx b/src/components/StatsBoxGrid/GridItem.tsx deleted file mode 100644 index c970b2358e3..00000000000 --- a/src/components/StatsBoxGrid/GridItem.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { - CategoryScale, - Chart as ChartJS, - Filler, - LinearScale, - LineElement, - PointElement, - ScriptableContext, -} from "chart.js" -import ChartDataLabels from "chartjs-plugin-datalabels" -import { Line } from "react-chartjs-2" -import { MdInfoOutline } from "react-icons/md" -import { Box, Flex, Icon, Text } from "@chakra-ui/react" - -import type { StatsBoxMetric, TimestampedData } from "@/lib/types" - -import { RANGES } from "@/lib/constants" - -import InlineLink from "../Link" -import OldText from "../OldText" -import StatErrorMessage from "../StatErrorMessage" -import Tooltip from "../Tooltip" -import Translation from "../Translation" - -const tooltipContent = (metric: StatsBoxMetric) => ( -
- {" "} - {metric.apiProvider} -
-) - -type GridItemProps = { - metric: StatsBoxMetric -} - -// ChartJS config -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Filler, - // to avoid a production error, we must include this plugin even if we do - // not use it (we are using it on the energy consumption chart) - ChartDataLabels -) - -export const GridItem = ({ metric }: GridItemProps) => { - const { title, description, state, buttonContainer, range } = metric - const hasError = "error" in state - const hasData = "data" in state - - const value = hasError ? ( - - ) : ( - - - {state.value}{" "} - - - - - - - - - - ) - - // Returns either 90 or 30-day data range depending on `range` selection - const filteredData = (data: TimestampedData[]) => { - if (range === RANGES[1]) return [...data] - - return data.filter(({ timestamp }) => { - const millisecondRange = 1000 * 60 * 60 * 24 * 30 - const now = new Date().getTime() - - return timestamp >= now - millisecondRange - }) - } - - const minValue = hasData - ? state.data.reduce( - (prev, { value }) => (prev < value ? prev : value), - Infinity - ) - : 0 - - const maxValue = hasData - ? state.data.reduce((prev, { value }) => (prev > value ? prev : value), 0) - : 0 - - // ChartJS options - const chartOptions = { - // chart styles - borderColor: "#8884db", - borderWidth: 1, - tension: 0.3, - fill: true, - backgroundColor: (context: ScriptableContext<"line">) => { - const ctx = context.chart.ctx - const gradient = ctx.createLinearGradient(0, 0, 0, 220) - // gradient.addColorStop(offset, color) - gradient.addColorStop(0, "#8884d84d") - gradient.addColorStop(0.85, "#ffffff00") - - return gradient - }, - pointRadius: 0, - maintainAspectRatio: false, - // chart legend/title config - plugins: { - legend: { - display: false, // hide chart legend - }, - title: { - display: false, // hide titles - }, - // force disabling chart labels because when the user do an internal - // navigation, labels are displayed incorrectly (probably a bug in - // chart.js or the react wrapper) - datalabels: { - display: false, - }, - }, - // chart labels config - scales: { - y: { - display: false, // hide Y axis labels - grid: { - display: false, - }, - min: minValue, - max: maxValue, - }, - x: { - display: false, // hide X axis labels - grid: { - display: false, - }, - }, - }, - } - - const filteredRange = filteredData(hasData ? state.data : []) - - const chartData = { - labels: filteredRange, - datasets: [ - { - data: filteredRange.map((item) => item.value), - }, - ], - } - - return ( - - - - {title} - - {description} - - {hasData && ( - - - - )} - - - {value} - - {hasData && ( - - {buttonContainer} - - )} - - - ) -} diff --git a/src/components/StatsBoxGrid/RangeSelector.tsx b/src/components/StatsBoxGrid/RangeSelector.tsx deleted file mode 100644 index f20c2bd0a9b..00000000000 --- a/src/components/StatsBoxGrid/RangeSelector.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Button } from "@chakra-ui/react" - -import { MatomoEventOptions, trackCustomEvent } from "@/lib/utils/matomo" - -import { RANGES } from "@/lib/constants" - -type RangeSelectorProps = { - state: string - setState: (state: string) => void - matomo: MatomoEventOptions -} - -export const RangeSelector = ({ - state, - setState, - matomo, -}: RangeSelectorProps) => ( -
- {RANGES.map((range, idx) => ( - - ))} -
-) diff --git a/src/components/StatsBoxGrid/index.tsx b/src/components/StatsBoxGrid/index.tsx index bda92a8f3a0..7b85bdd7372 100644 --- a/src/components/StatsBoxGrid/index.tsx +++ b/src/components/StatsBoxGrid/index.tsx @@ -1,31 +1,36 @@ -import { SimpleGrid } from "@chakra-ui/react" - import type { AllMetricData } from "@/lib/types" -import { GridItem } from "./GridItem" +import BigNumber from "../BigNumber" + import { useStatsBoxGrid } from "./useStatsBoxGrid" type StatsBoxGridProps = { - data: AllMetricData + metricResults: AllMetricData } +const StatsBoxGrid = ({ metricResults }: StatsBoxGridProps) => { + const metrics = useStatsBoxGrid(metricResults) -const StatsBoxGrid = ({ data }: StatsBoxGridProps) => { - const metrics = useStatsBoxGrid(data) - + const gridBorderClasses = [ + "border-b border-body-light xl:border-e xl:pe-8", + "border-b border-body-light xl:ps-8", + "border-b border-body-light xl:border-b-0 xl:border-e xl:pe-8", + "xl:ps-8", + ] return ( - - {metrics.map((metric, idx) => ( - +
+ {metrics.map(({ label, apiProvider, apiUrl, state }, idx) => ( + + {label} + ))} - +
) } diff --git a/src/components/StatsBoxGrid/useStatsBoxGrid.tsx b/src/components/StatsBoxGrid/useStatsBoxGrid.tsx index 1651b0a801a..afa150dc0ae 100644 --- a/src/components/StatsBoxGrid/useStatsBoxGrid.tsx +++ b/src/components/StatsBoxGrid/useStatsBoxGrid.tsx @@ -1,4 +1,7 @@ -import { useState } from "react" +/** + * TODO: Update metric for new homepage: + * - [ ] Replace TVL DeFi with "Total value held on Ethereum" + */ import { useRouter } from "next/router" import { useTranslation } from "next-i18next" @@ -6,170 +9,114 @@ import type { AllMetricData, Lang, StatsBoxMetric } from "@/lib/types" import { getLocaleForNumberFormat } from "@/lib/utils/translations" -import { RANGES } from "@/lib/constants" - -import { RangeSelector } from "./RangeSelector" - -const formatTotalStaked = (amount: number, locale: string): string => { +const formatLargeUSD = (value: number, locale: string): string => { return new Intl.NumberFormat(locale, { + style: "currency", + currency: "USD", notation: "compact", minimumSignificantDigits: 3, maximumSignificantDigits: 4, - }).format(amount) + }).format(value) } -const formatTVL = (tvl: number, locale: string): string => { +const formatSmallUSD = (value: number, locale: string): string => { return new Intl.NumberFormat(locale, { style: "currency", currency: "USD", notation: "compact", - minimumSignificantDigits: 3, - maximumSignificantDigits: 4, - }).format(tvl) + minimumSignificantDigits: 2, + maximumSignificantDigits: 3, + }).format(value) } -const formatTxs = (txs: number, locale: string): string => { +const formatLargeNumber = (value: number, locale: string): string => { return new Intl.NumberFormat(locale, { notation: "compact", minimumSignificantDigits: 3, maximumSignificantDigits: 4, - }).format(txs) -} - -const formatNodes = (nodes: number, locale: string): string => { - return new Intl.NumberFormat(locale, { - minimumSignificantDigits: 3, - maximumSignificantDigits: 4, - }).format(nodes) + }).format(value) } export const useStatsBoxGrid = ({ totalEthStaked, - nodeCount, totalValueLocked, txCount, + txCostsMedianUsd, + ethPrice, }: AllMetricData): StatsBoxMetric[] => { const { t } = useTranslation("page-index") const { locale } = useRouter() - const [selectedRangeTotalStaked, setSelectedRangeTotalStaked] = - useState(RANGES[0]) - const [selectedRangeTvl, setSelectedRangeTvl] = useState(RANGES[0]) - const [selectedRangeNodes, setSelectedRangeNodes] = useState( - RANGES[0] - ) - const [selectedRangeTxs, setSelectedRangeTxs] = useState(RANGES[0]) - const localeForNumberFormat = getLocaleForNumberFormat(locale! as Lang) - const totalEtherStaked = - "error" in totalEthStaked - ? { error: totalEthStaked.error } - : { - data: totalEthStaked.data, - value: formatTotalStaked(totalEthStaked.value, localeForNumberFormat), - } + const hasEthStakerAndPriceData = + "value" in totalEthStaked && "value" in ethPrice + const totalStakedInUsd = hasEthStakerAndPriceData + ? totalEthStaked.value * ethPrice.value + : 0 + + const totalEtherStaked = !totalStakedInUsd + ? { + error: + "error" in totalEthStaked + ? totalEthStaked.error + : "error" in ethPrice + ? ethPrice.error + : "", + } + : { + ...totalEthStaked, + value: formatLargeUSD(totalStakedInUsd, localeForNumberFormat), + } const valueLocked = "error" in totalValueLocked ? { error: totalValueLocked.error } : { - data: totalValueLocked.data, - value: formatTVL(totalValueLocked.value, localeForNumberFormat), + ...totalValueLocked, + value: formatLargeUSD(totalValueLocked.value, localeForNumberFormat), } const txs = "error" in txCount ? { error: txCount.error } : { - data: txCount.data, - value: formatTxs(txCount.value, localeForNumberFormat), + ...txCount, + value: formatLargeNumber(txCount.value, localeForNumberFormat), } - const nodes = - "error" in nodeCount - ? { error: nodeCount.error } + const medianTxCost = + "error" in txCostsMedianUsd + ? { error: txCostsMedianUsd.error } : { - data: nodeCount.data, - value: formatNodes(nodeCount.value, localeForNumberFormat), + ...txCostsMedianUsd, + value: formatSmallUSD(txCostsMedianUsd.value, localeForNumberFormat), } const metrics: StatsBoxMetric[] = [ { - apiProvider: "Dune Analytics", - apiUrl: "https://dune.com/", - title: t("page-index-network-stats-total-eth-staked"), - description: t("page-index-network-stats-total-eth-staked-explainer"), - buttonContainer: ( - - ), - state: totalEtherStaked, - range: selectedRangeTotalStaked, + apiProvider: "DeFi Llama", + apiUrl: "https://defillama.com/", + label: t("page-index-network-stats-value-defi-description"), + state: valueLocked, }, { - apiProvider: "Etherscan", - apiUrl: "https://etherscan.io/", - title: t("page-index-network-stats-tx-day-description"), - description: t("page-index-network-stats-tx-day-explainer"), - buttonContainer: ( - - ), + apiProvider: "GrowThePie", + apiUrl: "https://growthepie.xyz/", + label: t("page-index-network-stats-tx-day-description"), state: txs, - range: selectedRangeTxs, }, { - apiProvider: "DeFi Llama", - apiUrl: "https://defillama.com/", - title: t("page-index-network-stats-value-defi-description"), - description: t("page-index-network-stats-value-defi-explainer"), - buttonContainer: ( - - ), - state: valueLocked, - range: selectedRangeTvl, + apiProvider: "GrowThePie", + apiUrl: "https://growthepie.xyz/", + label: t("page-index-network-stats-tx-cost-description"), + state: medianTxCost, }, { - apiProvider: "Etherscan", - apiUrl: "https://etherscan.io/nodetracker", - title: t("page-index-network-stats-nodes-description"), - description: t("page-index-network-stats-nodes-explainer"), - buttonContainer: ( - - ), - state: nodes, - range: selectedRangeNodes, + apiProvider: "Dune Analytics", + apiUrl: "https://dune.com/", + label: t("page-index-network-stats-total-eth-staked"), + state: totalEtherStaked, }, ] diff --git a/src/components/Swiper/index.tsx b/src/components/Swiper/index.tsx new file mode 100644 index 00000000000..d235a381a03 --- /dev/null +++ b/src/components/Swiper/index.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from "next-i18next" +import { EffectCards, Keyboard, Navigation, Pagination } from "swiper/modules" +import { Swiper as SwiperParent, SwiperSlide } from "swiper/react" +import type { SwiperOptions } from "swiper/types" + +import { ChevronNext, ChevronPrev } from "@/components/Chevron" + +import { cn } from "@/lib/utils/cn" + +import "swiper/css" +import "swiper/css/navigation" +import "swiper/css/pagination" +import "swiper/css/effect-cards" + +type SwiperProps = { + children: React.ReactNode[] + options?: SwiperOptions + className?: string + swiperClass?: string + sliderClass?: string +} +const Swiper = ({ + children, + className, + swiperClass, + sliderClass, + options, +}: SwiperProps) => { + const { t } = useTranslation("common") + return ( +
+ + {children.map((child, index) => ( + + {child} + + ))} + + +
+ + +
+ ) +} + +export default Swiper diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index d1595b91c04..c7cc096e453 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -27,7 +27,7 @@ const ThemeProvider = ({ children }: Pick) => { const theme = useMemo(() => merge(customTheme, { direction }), [direction]) return ( { to, href, formattedDetails, - date, location, imageUrl, startDate, @@ -157,7 +156,6 @@ const UpcomingEventsList = () => { key={idx} title={title} href={to || href} - date={date} description={formattedDetails} location={location} imageUrl={imageUrl} diff --git a/src/components/WindowBox/index.tsx b/src/components/WindowBox/index.tsx new file mode 100644 index 00000000000..da52cb65960 --- /dev/null +++ b/src/components/WindowBox/index.tsx @@ -0,0 +1,29 @@ +import type { FC, ReactNode, SVGProps } from "react" + +import { cn } from "@/lib/utils/cn" + +type WindowBoxProps = { + title: ReactNode + Svg: FC> + children?: ReactNode + className?: string +} + +const WindowBox = ({ title, Svg, children, className }: WindowBoxProps) => ( +
+
+
+ +
+

{title}

+
+ {children} +
+) + +export default WindowBox diff --git a/src/components/icons/angle-brackets.svg b/src/components/icons/angle-brackets.svg new file mode 100644 index 00000000000..7dedd4d5cb6 --- /dev/null +++ b/src/components/icons/angle-brackets.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/icons/block-heap.svg b/src/components/icons/block-heap.svg new file mode 100644 index 00000000000..4319b7b9034 --- /dev/null +++ b/src/components/icons/block-heap.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/icons/calendar-add.svg b/src/components/icons/calendar-add.svg new file mode 100644 index 00000000000..89f48f3033d --- /dev/null +++ b/src/components/icons/calendar-add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/icons/calendar.svg b/src/components/icons/calendar.svg new file mode 100644 index 00000000000..aa365893c54 --- /dev/null +++ b/src/components/icons/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/icons/eth-glyph.svg b/src/components/icons/eth-glyph.svg new file mode 100644 index 00000000000..0d15d4f95d4 --- /dev/null +++ b/src/components/icons/eth-glyph.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/icons/eth-token.svg b/src/components/icons/eth-token.svg new file mode 100644 index 00000000000..0dbbf26a378 --- /dev/null +++ b/src/components/icons/eth-token.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/icons/eth-wallet.svg b/src/components/icons/eth-wallet.svg new file mode 100644 index 00000000000..dfa521e49a2 --- /dev/null +++ b/src/components/icons/eth-wallet.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/icons/layer-2.svg b/src/components/icons/layer-2.svg new file mode 100644 index 00000000000..c30ee34895a --- /dev/null +++ b/src/components/icons/layer-2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/icons/network-layers.svg b/src/components/icons/network-layers.svg new file mode 100644 index 00000000000..b78b04d247e --- /dev/null +++ b/src/components/icons/network-layers.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/icons/phone-homescreen.svg b/src/components/icons/phone-homescreen.svg new file mode 100644 index 00000000000..bc635f5e6d8 --- /dev/null +++ b/src/components/icons/phone-homescreen.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/components/icons/roadmap-sign.svg b/src/components/icons/roadmap-sign.svg new file mode 100644 index 00000000000..22114ed5704 --- /dev/null +++ b/src/components/icons/roadmap-sign.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/icons/whitepaper.svg b/src/components/icons/whitepaper.svg new file mode 100644 index 00000000000..0d96718efd1 --- /dev/null +++ b/src/components/icons/whitepaper.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/ui/buttons/Button.tsx b/src/components/ui/buttons/Button.tsx index 000d6736854..fbf4e1278d8 100644 --- a/src/components/ui/buttons/Button.tsx +++ b/src/components/ui/buttons/Button.tsx @@ -14,14 +14,15 @@ const buttonVariants = cva( variants: { variant: { solid: - "text-background bg-primary border-transparent disabled:bg-disabled disabled:text-background hover:text-background hover:bg-primary-hover hover:shadow-button-hover active:shadow-none", - outline: "hover:shadow-button-hover active:shadow-none", + "text-white bg-primary-action border-transparent disabled:bg-disabled disabled:text-background hover:text-white hover:bg-primary-action-hover hover:shadow-button-hover active:shadow-none", + outline: "hover:shadow-button-hover active:shadow-none text-body", "outline-color": "hover:shadow-button-hover active:shadow-none border-primary", ghost: "border-transparent", link: "border-transparent font-bold underline py-0 px-1 active:text-primary", }, size: { + lg: "text-lg py-3 px-8 [&>svg]:text-2xl rounded-lg", md: "min-h-10.5 px-4 py-2 [&>svg]:text-2xl", sm: "text-xs min-h-[31px] py-1.5 px-2 [&>svg]:text-md", }, diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 00000000000..1fc23d96378 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,157 @@ +import * as React from "react" +import { cva, VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils/cn" + +import { BaseLink } from "./Link" + +const titleVariants = cva( + "group-hover/link:underline group-focus/link:underline", + { + variants: { + variant: { + bold: "text-2xl font-bold", + black: "text-3xl font-black", + }, + }, + defaultVariants: { + variant: "bold", + }, + } +) + +type CardProps = React.HTMLAttributes & { + href?: string +} +const Card = React.forwardRef( + ({ className, href, ...props }, ref) => { + if (href) { + return ( + +
+ + ) + } + return ( +
+ ) + } +) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardBanner = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardBanner.displayName = "CardBanner" + +const CardTitle = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardSubTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardSubTitle.displayName = "CardSubTitle" + +const CardHighlight = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardHighlight.displayName = "CardHighlight" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { + Card, + CardBanner, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardHighlight, + CardSubTitle, + CardTitle, +} diff --git a/src/components/ui/section.tsx b/src/components/ui/section.tsx new file mode 100644 index 00000000000..565d22c86c1 --- /dev/null +++ b/src/components/ui/section.tsx @@ -0,0 +1,82 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils/cn" + +// TODO: Add to design system +const variants = cva("w-full scroll-mt-24", { + variants: { + variant: { + responsiveFlex: "flex flex-col gap-8 md:flex-row lg:gap-16", + }, + }, +}) + +const Section = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Section.displayName = "Section" + +const SectionBanner = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +SectionBanner.displayName = "SectionBanner" + +const SectionHeader = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +SectionHeader.displayName = "SectionHeader" + +const SectionTag = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +SectionTag.displayName = "SectionTag" + +const SectionContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +SectionContent.displayName = "SectionContent" + +export { Section, SectionBanner, SectionContent, SectionHeader, SectionTag } diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 00000000000..de142751443 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,56 @@ +import { cn } from "@/lib/utils/cn" + +// Pseudo-random list of skeleton widths for multiple lines +const widths = [ + "w-1/3", + "w-1/5", + "w-4", + "w-1/4", + "w-1/2", + "w-1/6", + "w-8", + "w-1/4", + "w-1/3", + "w-1/5", + "w-1/6", + "w-4", + "w-1/4", + "w-1/3", + "w-1/2", + "w-1/5", +] + +const Skeleton = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( +
+ ) +} + +type SkeletonLinesProps = React.HTMLAttributes & { + noOfLines?: number +} + +const SkeletonLines = ({ + className, + noOfLines = 1, + ...props +}: SkeletonLinesProps) => ( +
+ {Array(noOfLines) + .fill(0) + .map((_, idx) => ( + + ))} +
+) + +export { Skeleton, SkeletonLines } diff --git a/src/data/community-events.json b/src/data/community-events.json index 8b1045c4742..98bbe0249f3 100644 --- a/src/data/community-events.json +++ b/src/data/community-events.json @@ -520,7 +520,7 @@ "href": "https://ethmumbai.in", "location": "Mumbai, IND", "description": "First ever Ethereum Hackathon in Mumbai. Build from Mumbai, for the world. 29-31 March 2024.", - "imageUrl": "" + "imageUrl": "https://ethmumbai.in/post.png" }, { "title": "ETH Gathering", @@ -565,7 +565,7 @@ "href": "https://ethwarsaw.dev", "location": "Warsaw, POL", "description": "EthWarsaw 2024", - "imageUrl": "" + "imageUrl": "https://cdn.prod.website-files.com/649014d99c5194ad73558cd3/649af7b1f1fdaa7868890166_ThumbnailPicture.png" }, { "title": "NapulETH", diff --git a/src/data/placeholders/content-contributing-translation-program-translatathon-translatathon-hubs-data.json b/src/data/placeholders/content-contributing-translation-program-translatathon-translatathon-hubs-data.json new file mode 100644 index 00000000000..34cfd9c5b0b --- /dev/null +++ b/src/data/placeholders/content-contributing-translation-program-translatathon-translatathon-hubs-data.json @@ -0,0 +1,6 @@ +{ + "/content/contributing/translation-program/translatathon/translatathon-hubs/local-communities.png": { + "hash": "d0fbc65b", + "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAGCAYAAADKfB7nAAAACXBIWXMAABYlAAAWJQFJUiTwAAABkUlEQVR4nAGGAXn+AAAANPsHFkf8Fg8y/EcdOvy+jpT88cjC/K+QmPyljqH848TS/P/ZyPx/cYP8R0p+/AAKR/xjVn/8b0li/BwMOPoAAAA4/xIgU/8YFTv/gEBV/3xum/9dhNb/ZWOf/3+Pv/+di7//e3bI/1Oz1/8mSX//AAA//0w5af84Hkn/IhA6/wAABDz/Dx9S/0EsTP+GSFz/p2hy/7fF2f+xttH/5Nfd/8uqw/+0tOT/zYmI/4J4n/+Yg47/t2Vd/zwpVv86JE3/AAAPSv8LI2D/FhtV/w0VUf9bQGv/n36k/5aKq//2+v//0fr//2likv/Nj6f/04Wh/3lacv9SKUj/AhFP/ykjWf8AAR5b/xMoaf80SI//M0eS/4NtpP+jb6H/l6fO/73e//++2f//YGnc/5h/qv+tYWb/o2hw/4uCp/9MSX7/ABhf/wANLGn5ARZS+ztEgPpPV5L8R1ef+1NeqPqGeqz6cn/F+k1fufpPOoD8f1Fx/8J4cf/7mW7/04t4+1VFcPsAFVb0G7fcN+dEqnEAAAAASUVORK5CYII=" + } +} \ No newline at end of file diff --git a/src/hooks/useRtlFlip.ts b/src/hooks/useRtlFlip.ts index b3a39369818..3c684d06800 100644 --- a/src/hooks/useRtlFlip.ts +++ b/src/hooks/useRtlFlip.ts @@ -7,7 +7,7 @@ import { isLangRightToLeft } from "@/lib/utils/translations" type UseDirection = { /** @deprecated */ flipForRtl: "scaleX(-1)" | undefined // transform (deprecated) - twFlipForRtl: "-scale-x-1" | "" // className + twFlipForRtl: "-scale-x-100" | "" // className isRtl: boolean direction: "ltr" | "rtl" } @@ -22,7 +22,7 @@ export const useRtlFlip = (): UseDirection => { const isRtl = isLangRightToLeft(locale as Lang) return { flipForRtl: isRtl ? "scaleX(-1)" : undefined, // transform (deprecated) - twFlipForRtl: isRtl ? "-scale-x-1" : "", // className (preferred) + twFlipForRtl: isRtl ? "-scale-x-100" : "", // className (preferred) isRtl, direction: isRtl ? "rtl" : "ltr", } diff --git a/src/intl/en/common.json b/src/intl/en/common.json index 4eb2fc148cd..134b022c7b7 100644 --- a/src/intl/en/common.json +++ b/src/intl/en/common.json @@ -198,6 +198,7 @@ "languages": "Languages", "last-24-hrs": "Last 24 hours", "last-edit": "Last edit", + "last-updated": "Last updated", "layer-2": "Layer 2", "learn": "Learn", "learn-by-coding": "Learn by coding", @@ -431,4 +432,4 @@ "wrapped-ether": "Wrapped Ether", "yes": "Yes", "zero-knowledge-proofs": "Zero-knowledge proofs" -} +} \ No newline at end of file diff --git a/src/intl/en/page-index.json b/src/intl/en/page-index.json index 9a4befae773..d4c87175069 100644 --- a/src/intl/en/page-index.json +++ b/src/intl/en/page-index.json @@ -1,10 +1,23 @@ { - "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.", - "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.", - "page-index-meta-title": "Home", - "page-index-title": "Welcome to Ethereum", - "page-index-description": "Ethereum is the community-run technology powering the cryptocurrency ether (ETH) and thousands of decentralized applications.", "page-index-title-button": "Explore Ethereum", + "page-index-touts-header": "Explore ethereum.org", + "page-index-contribution-banner-title": "Contribute to ethereum.org", + "page-index-contribution-banner-description": "This website is open source with hundreds of community contributors. You can propose edits to any of the content on this site, suggest awesome new features, or help us squash bugs.", + "page-index-contribution-banner-image-alt": "An Ethereum logo made of lego bricks.", + "page-index-contribution-banner-button": "More on contributing", + "page-index-tout-upgrades-title": "Level up your upgrade knowledge", + "page-index-tout-upgrades-description": "The Ethereum roadmap consists of interconnected upgrades designed to make the network more scalable, secure, and sustainable.", + "page-index-tout-upgrades-image-alt": "Illustration of a spaceship representing the increased power after Ethereum upgrades.", + "page-index-tout-enterprise-title": "Ethereum for enterprise", + "page-index-tout-enterprise-description": "See how Ethereum can open up new business models, reduce your costs and future-proof your business.", + "page-index-tout-enterprise-image-alt": "Illustration of a futuristic computer/device.", + "page-index-tout-community-title": "The Ethereum community", + "page-index-tout-community-description": "Ethereum is all about community. It's made up of people from all different backgrounds and interests. See how you can join in.", + "page-index-tout-community-image-alt": "Illustration of a group of builders working together.", + "page-index-nft": "The internet of assets", + "page-index-nft-description": "Ethereum isn't just for digital money. Anything you can own can be represented, traded and put to use as non-fungible tokens (NFTs). You can tokenise your art and get royalties automatically every time it's re-sold. Or use a token for something you own to take out a loan. The possibilities are growing all the time.", + "page-index-nft-button": "More on NFTs", + "page-index-nft-alt": "An Eth logo being displayed via hologram.", "page-index-get-started": "Get started", "page-index-get-started-description": "ethereum.org is your portal into the world of Ethereum. The tech is new and ever-evolving – it helps to have a guide. Here's what we recommend you do if you want to dive in.", "page-index-get-started-image-alt": "Illustration of a person working on a computer.", @@ -37,45 +50,8 @@ "page-index-developers": "A new frontier for development", "page-index-developers-description": "Ethereum and its apps are transparent and open source. You can fork code and re-use functionality others have already built. If you don't want to learn a new language you can just interact with open-sourced code using JavaScript and other existing languages.", "page-index-developers-button": "Developer portal", - "page-index-developers-code-examples": "Code examples", - "page-index-developers-code-example-title-0": "Your own bank", - "page-index-developers-code-example-description-0": "You can build a bank powered by logic you've programmed.", - "page-index-developers-code-example-title-1": "Your own currency", - "page-index-developers-code-example-description-1": "You can create tokens that you can transfer and use across applications.", - "page-index-developers-code-example-title-2": "A JavaScript Ethereum wallet", - "page-index-developers-code-example-description-2": "You can use existing languages to interact with Ethereum and other applications.", - "page-index-developers-code-example-title-3": "An open, permissionless DNS", - "page-index-developers-code-example-description-3": "You can reimagine existing services as decentralized, open applications.", - "page-index-network-stats-title": "Ethereum today", - "page-index-network-stats-subtitle": "The latest network statistics", - "page-index-network-stats-total-eth-staked": "Total ETH staked", - "page-index-network-stats-eth-price-description": "ETH price (USD)", - "page-index-network-stats-eth-price-explainer": "The latest price for 1 ether. You can buy as little as 0.000000000000000001 – you don't need to buy 1 whole ETH.", - "page-index-network-stats-total-eth-staked-explainer": "The total amount of ETH currently being staked and securing the network.", - "page-index-network-stats-tx-day-description": "Transactions today", - "page-index-network-stats-tx-day-explainer": "The number of transactions successfully processed on the network in the last 24 hours.", - "page-index-network-stats-value-defi-description": "Value locked in DeFi (USD)", - "page-index-network-stats-value-defi-explainer": "The amount of money in decentralized finance (DeFi) applications, the Ethereum digital economy.", "page-index-network-stats-nodes-description": "Nodes", "page-index-network-stats-nodes-explainer": "Ethereum is run by thousands of volunteers around the globe, known as nodes.", - "page-index-touts-header": "Explore ethereum.org", - "page-index-contribution-banner-title": "Contribute to ethereum.org", - "page-index-contribution-banner-description": "This website is open source with hundreds of community contributors. You can propose edits to any of the content on this site, suggest awesome new features, or help us squash bugs.", - "page-index-contribution-banner-image-alt": "An Ethereum logo made of lego bricks.", - "page-index-contribution-banner-button": "More on contributing", - "page-index-tout-upgrades-title": "Level up your upgrade knowledge", - "page-index-tout-upgrades-description": "The Ethereum roadmap consists of interconnected upgrades designed to make the network more scalable, secure, and sustainable.", - "page-index-tout-upgrades-image-alt": "Illustration of a spaceship representing the increased power after Ethereum upgrades.", - "page-index-tout-enterprise-title": "Ethereum for enterprise", - "page-index-tout-enterprise-description": "See how Ethereum can open up new business models, reduce your costs and future-proof your business.", - "page-index-tout-enterprise-image-alt": "Illustration of a futuristic computer/device.", - "page-index-tout-community-title": "The Ethereum community", - "page-index-tout-community-description": "Ethereum is all about community. It's made up of people from all different backgrounds and interests. See how you can join in.", - "page-index-tout-community-image-alt": "Illustration of a group of builders working together.", - "page-index-nft": "The internet of assets", - "page-index-nft-description": "Ethereum isn't just for digital money. Anything you can own can be represented, traded and put to use as non-fungible tokens (NFTs). You can tokenise your art and get royalties automatically every time it's re-sold. Or use a token for something you own to take out a loan. The possibilities are growing all the time.", - "page-index-nft-button": "More on NFTs", - "page-index-nft-alt": "An Eth logo being displayed via hologram.", "community-events-content-heading": "Join the ethereum.org community", "community-events-content-1": "Join almost 40 000 members on our Discord server.", "community-events-content-2": "Join our monthly community calls for exciting updates on Ethereum.org development and important ecosystem news. Get the chance to ask questions, share ideas, and provide feedback - it's the perfect opportunity to be part of the thriving Ethereum community.", @@ -85,5 +61,92 @@ "community-events-no-upcoming-calls": "No upcoming calls", "community-events-previous-calls": "Previous calls", "community-events-there-are-no-past-calls": "There are no past calls", - "community-events-add-to-calendar": "Add to calendar" + "community-events-add-to-calendar": "Add to calendar", + "page-index-activity-description": "Activity from all Ethereum networks", + "page-index-activity-tag": "Activity", + "page-index-activity-header": "The strongest ecosystem", + "page-index-bento-header": "A new way to use the internet", + "page-index-bento-assets-action": "More on NFTs", + "page-index-bento-assets-content": "Art, certificates or even real estate can be tokenized. Anything can be a tradable token. Ownership is public and verifiable.", + "page-index-bento-assets-title": "The internet of assets", + "page-index-bento-dapps-action": "Browse apps", + "page-index-bento-dapps-content": "Ethereum apps work without selling your data. Protect your privacy.", + "page-index-bento-dapps-title": "Innovative apps", + "page-index-bento-defi-action": "Explore DeFi", + "page-index-bento-defi-content": "Billions can't open bank accounts or freely use their money. Ethereum's financial system is always open and unbiased.", + "page-index-bento-defi-title": "A fairer financial system", + "page-index-bento-networks-action": "Explore benefits", + "page-index-bento-networks-content": "Ethereum is the hub for blockchain innovation. The best project are built on Ethereum.", + "page-index-bento-networks-title": "The network of networks", + "page-index-bento-stablecoins-action": "Learn more", + "page-index-bento-stablecoins-content": "Stablecoins are currencies that maintain stable value. Their price matches the U.S. dollar or other steady asset.", + "page-index-bento-stablecoins-title": "Crypto without volatility", + "page-index-builders-action-primary": "Builder's Portal", + "page-index-builders-action-secondary": "Documentation", + "page-index-builders-description": "Ethereum is home to Web3's largest and most vibrant developer ecosystem. Use JavaScript and Python, or learn a smart contract language like Solidity or Vyper to write your own app.", + "page-index-builders-tag": "Builders", + "page-index-builders-header": "Blockchain's biggest builder community", + "page-index-calendar-add": "Add to calendar", + "page-index-calendar-fallback": "No upcoming calls", + "page-index-calendar-title": "Next calls", + "page-index-community-action": "More on ethereum.org", + "page-index-community-description-1": "The ethereum.org website is built and maintained by hundreds of translators, coders, designers, copywriters, and enthusiastic community members each month.", + "page-index-community-description-2": "Come ask questions, connect with people around the world and contribute to the website. You will get relevant practical experience and be guided during the process!", + "page-index-community-description-3": "Ethereum.org community is the perfect place to start and learn.", + "page-index-community-tag": "Ethereum.org Community", + "page-index-community-header": "Built by the community", + "page-index-cta-dapps-description": "See what Ethereum can do", + "page-index-cta-dapps-label": "Try apps", + "page-index-cta-get-eth-description": "The currency of Ethereum", + "page-index-cta-get-eth-label": "Get ETH", + "page-index-cta-networks-description": "Enjoy minimal fees", + "page-index-cta-networks-label": "Choose a network", + "page-index-cta-wallet-description": "Create accounts, manage assets", + "page-index-cta-wallet-label": "Pick a wallet", + "page-index-description": "The leading platform for innovative apps and Ethereum-backed blockchain networks", + "page-index-developers-code-example-description-0": "Build a bank powered by logic you've programmed", + "page-index-developers-code-example-description-1": "Create tokens that you can transfer and use across applications", + "page-index-developers-code-example-description-2": "Use existing languages to interact with Ethereum and other applications", + "page-index-developers-code-example-description-3": "Reimagine existing services as decentralized, open applications", + "page-index-developers-code-example-title-0": "Your own bank", + "page-index-developers-code-example-title-1": "Your own currency", + "page-index-developers-code-example-title-2": "A JavaScript Ethereum wallet", + "page-index-developers-code-example-title-3": "An open, permissionless DNS", + "page-index-developers-code-examples": "Code examples", + "page-index-events-action": "See all events", + "page-index-events-header": "Events", + "page-index-events-subtitle": "Ethereum communities host events all around the globe, all year long", + "page-index-hero-image-alt": "An illustration of a futuristic city, representing the Ethereum ecosystem.", + "page-index-join-action-contribute-description": "Find out all the different ways you can help ethereum.org grow and be better.", + "page-index-join-action-contribute-label": "How to contribute", + "page-index-join-action-discord-description": "To ask questions, coordinate contribution and join community calls.", + "page-index-join-action-github-description": "Contribute to code, content, articles, etc.", + "page-index-join-action-twitter-description": "To keep up with our updates and important news.", + "page-index-join-description": "This website is open source with hundreds of community contributors. You can propose edits to any of the content on this site.", + "page-index-join-header": "Join ethereum.org", + "page-index-learn-description": "Crypto can feel overwhelming. Don't worry, these materials are designed to help you understand Ethereum in just a few minutes.", + "page-index-learn-tag": "Learn", + "page-index-learn-header": "Understand Ethereum", + "page-index-meta-description": "Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.", + "page-index-meta-title": "Home", + "page-index-network-stats-subtitle": "The latest network statistics", + "page-index-network-stats-title": "Ethereum today", + "page-index-network-stats-total-eth-staked-explainer": "The total amount of ETH currently being staked and securing the network.", + "page-index-network-stats-total-eth-staked": "Value protecting Ethereum", + "page-index-network-stats-tx-cost-description": "Average transaction cost", + "page-index-network-stats-tx-day-description": "Transactions in the last 24h", + "page-index-network-stats-tx-day-explainer": "The number of transactions successfully processed on the network in the last 24 hours.", + "page-index-network-stats-value-defi-description": "Value locked in DeFi (USD)", + "page-index-network-stats-value-defi-explainer": "The amount of money in decentralized finance (DeFi) applications, the Ethereum digital economy.", + "page-index-popular-topics-ethereum": "What is Ethereum?", + "page-index-popular-topics-header": "Popular topics", + "page-index-popular-topics-action": "Other topics", + "page-index-popular-topics-roadmap": "Ethereum roadmap", + "page-index-popular-topics-start": "How to start, step by step", + "page-index-popular-topics-wallets": "What are crypto wallets?", + "page-index-popular-topics-whitepaper": "Ethereum Whitepaper", + "page-index-posts-action": "Read more on these websites", + "page-index-posts-header": "Recent posts", + "page-index-posts-subtitle": "The latest blog posts and updates from the community", + "page-index-title": "Welcome to Ethereum" } diff --git a/src/lib/api/fetchEthPrice.ts b/src/lib/api/fetchEthPrice.ts new file mode 100644 index 00000000000..a86f6e1d8cc --- /dev/null +++ b/src/lib/api/fetchEthPrice.ts @@ -0,0 +1,17 @@ +import { MetricReturnData } from "../types" + +export const fetchEthPrice = async (): Promise => { + try { + const data: { ethereum: { usd: number } } = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" + ).then((res) => res.json()) + const { + ethereum: { usd }, + } = data + if (!usd) throw new Error("Unable to fetch ETH price from CoinGecko") + return { value: usd, timestamp: Date.now() } + } catch (error: unknown) { + console.error((error as Error).message) + return { error: (error as Error).message } + } +} diff --git a/src/lib/api/fetchGrowThePie.ts b/src/lib/api/fetchGrowThePie.ts new file mode 100644 index 00000000000..1affc794c3b --- /dev/null +++ b/src/lib/api/fetchGrowThePie.ts @@ -0,0 +1,67 @@ +import type { GrowThePieData } from "../types" + +type DataItem = { + metric_key: string + origin_key: string + date: string + value: number +} + +const TXCOSTS_MEDIAN_USD = "txcosts_median_usd" +const TXCOUNT = "txcount" + +export const fetchGrowThePie = async (): Promise => { + const url = "https://api.growthepie.xyz/v1/fundamentals_full.json" + try { + const response = await fetch(url) + if (!response.ok) { + console.log(response.status, response.statusText) + throw new Error("Failed to fetch GrowThePie data") + } + const data: DataItem[] = await response.json() + + const mostRecentDate = data.reduce((latest, item) => { + const itemDate = new Date(item.date) + return itemDate > new Date(latest) ? item.date : latest + }, data[0].date) + + const mostRecentData = data.filter( + (item) => + item.date === mostRecentDate && + [TXCOSTS_MEDIAN_USD, TXCOUNT].includes(item.metric_key) + ) + + let totalTxCount = 0 + let weightedSum = 0 + + mostRecentData.forEach((item) => { + if (item.metric_key !== TXCOSTS_MEDIAN_USD) return + + const txCountItem = mostRecentData.find( + (txItem) => + txItem.metric_key === TXCOUNT && txItem.origin_key === item.origin_key + ) + if (!txCountItem) return + + totalTxCount += txCountItem.value + weightedSum += item.value * txCountItem.value + }) + + // The weighted average of txcosts_median_usd, by txcount on each network (origin_key) + const weightedAverage = totalTxCount ? weightedSum / totalTxCount : 0 + + // Last updated timestamp + const timestamp = Date.now() + + return { + txCount: { value: totalTxCount, timestamp }, + txCostsMedianUsd: { value: weightedAverage, timestamp }, + } + } catch (error: unknown) { + console.error((error as Error).message) + return { + txCount: { error: (error as Error).message }, + txCostsMedianUsd: { error: (error as Error).message }, + } + } +} diff --git a/src/lib/api/fetchNodes.ts b/src/lib/api/fetchNodes.ts deleted file mode 100644 index 37f22e7f9ec..00000000000 --- a/src/lib/api/fetchNodes.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { - EtherscanNodeResponse, - MetricReturnData, - TimestampedData, -} from "@/lib/types" - -import { DAYS_TO_FETCH, ETHERSCAN_API_URL } from "@/lib/constants" - -export const fetchNodes = async (): Promise => { - const apiKey = process.env.ETHERSCAN_API_KEY - const now = new Date() - const endDate = now.toISOString().split("T")[0] // YYYY-MM-DD - const startDate = new Date(now.setDate(now.getDate() - DAYS_TO_FETCH)) - .toISOString() - .split("T")[0] // {daysToFetch} days ago - - const queryParams = new URLSearchParams({ - module: "stats", - action: "nodecounthistory", - startdate: startDate, - enddate: endDate, - sort: "desc", - apikey: apiKey, - }).toString() - - const { href } = new URL(`api?${queryParams}`, ETHERSCAN_API_URL) - - try { - const response = await fetch(href) - if (!response.ok) { - console.log(response.status, response.statusText) - throw new Error("Failed to fetch Etherscan node data") - } - - const json: EtherscanNodeResponse = await response.json() - const data: TimestampedData[] = json.result - .map(({ UTCDate, TotalNodeCount }) => ({ - timestamp: new Date(UTCDate).getTime(), - value: +TotalNodeCount, - })) - .sort((a, b) => a.timestamp - b.timestamp) - const { value } = data[data.length - 1] - - return { - data, // historical data: { timestamp: unix-milliseconds, value } - value, // current value (number, unformatted) - } - } catch (error: unknown) { - console.error((error as Error).message) - return { error: (error as Error).message } - } -} diff --git a/src/lib/api/fetchPosts.ts b/src/lib/api/fetchPosts.ts new file mode 100644 index 00000000000..31b1bea95a6 --- /dev/null +++ b/src/lib/api/fetchPosts.ts @@ -0,0 +1,39 @@ +import type { HTMLResult, RSSItem } from "../types" + +import { fetchXml } from "./fetchRSS" + +export const fetchAttestantPosts = async () => { + const BASE_URL = "https://www.attestant.io/posts/" + const htmlData = (await fetchXml(BASE_URL)) as HTMLResult + + // Extract div containing list of posts from deeply nested HTML structure + const postsContainer = + htmlData.html.body[0].div[0].div[1].div[0].div[0].div[0].div + + const posts: RSSItem[] = postsContainer + .map(({ a }) => { + const [ + { + $: { href }, + h4: [{ _: title }], + div: [{ _: content }, { _: pubDate }], + }, + ] = a + const { href: link } = new URL(href, BASE_URL) + return { + title, + link, + content, + source: "Attestant", + sourceUrl: BASE_URL, + sourceFeedUrl: BASE_URL, + imgSrc: "/images/attestant-logo.svg", + pubDate, + } + }) + .sort( + (a: RSSItem, b: RSSItem) => + new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime() + ) + return posts +} diff --git a/src/lib/api/fetchRSS.ts b/src/lib/api/fetchRSS.ts new file mode 100644 index 00000000000..f191e3091bf --- /dev/null +++ b/src/lib/api/fetchRSS.ts @@ -0,0 +1,138 @@ +import { parseString } from "xml2js" + +import type { + AtomElement, + AtomResult, + RSSChannel, + RSSItem, + RSSResult, +} from "../types" +import { isValidDate } from "../utils/date" + +/** + * Fetches RSS feed from the specified XML URL(s). + * @param xmlUrl - The URL(s) of the XML feed to fetch. + * @returns An array sources, each containing an array of RSS items + */ +export const fetchRSS = async (xmlUrl: string | string[]) => { + const urls = Array.isArray(xmlUrl) ? xmlUrl : [xmlUrl] + const allItems: RSSItem[][] = [] + for (const url of urls) { + const response = (await fetchXml(url)) as RSSResult | AtomResult + if ("rss" in response) { + const [mainChannel] = response.rss.channel as RSSChannel[] + const [source] = mainChannel.title + const [sourceUrl] = mainChannel.link + const channelImage = mainChannel.image ? mainChannel.image[0].url[0] : "" + + const parsedRssItems = mainChannel.item + // Filter out items with invalid dates + .filter((item) => { + if (!item.pubDate) return false + const [pubDate] = item.pubDate + return isValidDate(pubDate) + }) + // Sort by pubDate (most recent is first in array + .sort((a, b) => { + const dateA = new Date(a.pubDate[0]) + const dateB = new Date(b.pubDate[0]) + return dateB.getTime() - dateA.getTime() + }) + // Map to RSSItem object + .map((item) => { + const getImgSrc = () => { + if (item.enclosure) return item.enclosure[0].$.url + if (item["media:content"]) return item["media:content"][0].$.url + return channelImage + } + return { + pubDate: item.pubDate[0], + title: item.title[0], + link: item.link[0], + imgSrc: getImgSrc(), + source, + sourceUrl, + sourceFeedUrl: url, + } as RSSItem + }) + + allItems.push(parsedRssItems) + } else if ("feed" in response) { + const [source] = response.feed.title + const [sourceUrl] = response.feed.id + const feedImage = response.feed.icon?.[0] + + const parsedAtomItems = response.feed.entry + // Filter out items with invalid dates + .filter((entry) => { + if (!entry.updated) return false + const [published] = entry.updated + return isValidDate(published) + }) + // Sort by published (most recent is first in array + .sort((a, b) => { + const dateA = new Date(a.updated[0]) + const dateB = new Date(b.updated[0]) + return dateB.getTime() - dateA.getTime() + }) + // Map to RSSItem object + .map((entry) => { + const getString = (el?: AtomElement[]): string => { + if (!el) return "" + const [firstEl] = el + if (typeof firstEl === "string") return firstEl + return firstEl._ || "" + } + const getHref = (): string => { + if (!entry.link) { + console.warn(`No link found for RSS url: ${url}`) + return "" + } + const link = entry.link[0] + if (typeof link === "string") return link + return link.$.href || "" + } + const getImgSrc = (): string => { + const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g + const contentMatch = getString(entry.content).match(imgRegEx) + if (contentMatch) return contentMatch[0] + const summaryMatch = getString(entry.summary).match(imgRegEx) + if (summaryMatch) return summaryMatch[0] + return feedImage || "" + } + return { + pubDate: entry.updated[0], + title: getString(entry.title), + link: getHref(), + imgSrc: getImgSrc(), + source, + sourceUrl, + sourceFeedUrl: url, + } as RSSItem + }) + + allItems.push(parsedAtomItems) + } + } + return allItems as RSSItem[][] +} + +/** + * Fetches XML data from the specified URL. + * Parses XML to JSON with parseString (xml2js package) + * @param url - The URL to fetch the XML data from. + * @returns A promise that resolves to the parsed XML data as a JSON object. + */ +export const fetchXml = async (url: string) => { + const response = await fetch(url) + const xml = await response.text() + let returnObject: Record = {} + parseString(xml, (err, result) => { + if (err) { + console.error(err) + return + } + returnObject = result + }) + return returnObject +} diff --git a/src/lib/api/fetchTotalEthStaked.ts b/src/lib/api/fetchTotalEthStaked.ts index 87953e83d15..7828977b5d1 100644 --- a/src/lib/api/fetchTotalEthStaked.ts +++ b/src/lib/api/fetchTotalEthStaked.ts @@ -13,33 +13,23 @@ export const fetchTotalEthStaked = async (): Promise => { const url = new URL("api/v1/query/3915587/results", DUNE_API_URL) try { - const ethStakedResponse = await fetch(url, { + const response = await fetch(url, { headers: { "X-Dune-API-Key": DUNE_API_KEY }, }) - if (!ethStakedResponse.ok) { - console.log(ethStakedResponse.status, ethStakedResponse.statusText) + if (!response.ok) { + console.log(response.status, response.statusText) throw new Error("Failed to fetch eth staked data") } - const ethStakedJson: EthStakedResponse = await ethStakedResponse.json() + const json: EthStakedResponse = await response.json() const { result: { rows = [] }, - } = ethStakedJson + } = json + // Today's value at start of array + const value = rows[0].cum_deposited_eth - const data = rows.map((row) => ({ - timestamp: new Date(row.time).getTime(), - value: row.cum_deposited_eth, - })) - - // data is already sorted...but just in case - data.sort((a, b) => a.timestamp - b.timestamp) - - const { value } = data[data.length - 1] - - return { - data, - value, - } + // current value (number, unformatted) + return { value, timestamp: Date.now() } } catch (error: unknown) { console.error((error as Error).message) return { error: (error as Error).message } diff --git a/src/lib/api/fetchTotalValueLocked.ts b/src/lib/api/fetchTotalValueLocked.ts index 5299b86792b..cf9cbb1a256 100644 --- a/src/lib/api/fetchTotalValueLocked.ts +++ b/src/lib/api/fetchTotalValueLocked.ts @@ -1,14 +1,6 @@ -import takeRightWhile from "lodash/takeRightWhile" - import { DefiLlamaTVLResponse, MetricReturnData } from "@/lib/types" -import { DAYS_TO_FETCH } from "@/lib/constants" - export const fetchTotalValueLocked = async (): Promise => { - const now = new Date() - const startDate = new Date(now.setDate(now.getDate() - DAYS_TO_FETCH)) - const startTimestamp = Math.round(startDate.getTime() / 1000) - try { const response = await fetch(`https://api.llama.fi/charts/Ethereum`) if (!response.ok) { @@ -17,18 +9,11 @@ export const fetchTotalValueLocked = async (): Promise => { } const json: DefiLlamaTVLResponse = await response.json() - const data = takeRightWhile(json, ({ date }) => +date > startTimestamp) - .map(({ date, totalLiquidityUSD }) => ({ - timestamp: +date * 1000, - value: totalLiquidityUSD, - })) - .sort((a, b) => a.timestamp - b.timestamp) - const { value } = data[data.length - 1] + // Today's value at end of array + const value = json[json.length - 1].totalLiquidityUSD - return { - data, // historical data: { timestamp: unix-milliseconds, value } - value, // current value (number, unformatted) - } + // current value (number, unformatted) + return { value, timestamp: Date.now() } } catch (error: unknown) { console.error((error as Error).message) return { error: (error as Error).message } diff --git a/src/lib/api/fetchTxCount.ts b/src/lib/api/fetchTxCount.ts deleted file mode 100644 index a68c2f633df..00000000000 --- a/src/lib/api/fetchTxCount.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { - EtherscanTxCountResponse, - MetricReturnData, - TimestampedData, -} from "@/lib/types" - -import { DAYS_TO_FETCH, ETHERSCAN_API_URL } from "@/lib/constants" - -export const fetchTxCount = async (): Promise => { - const apiKey = process.env.ETHERSCAN_API_KEY - const now = new Date() - const endDate = now.toISOString().split("T")[0] // YYYY-MM-DD - const startDate = new Date(now.setDate(now.getDate() - DAYS_TO_FETCH)) - .toISOString() - .split("T")[0] // {DAYS_TO_FETCH} days ago - - const queryParams = new URLSearchParams({ - module: "stats", - action: "dailytx", - startdate: startDate, - enddate: endDate, - sort: "asc", - apikey: apiKey, - }).toString() - - const { href } = new URL(`api?${queryParams}`, ETHERSCAN_API_URL) - - try { - const response = await fetch(href) - if (!response.ok) { - console.log(response.status, response.statusText) - throw new Error("Failed to fetch Etherscan tx count data") - } - - const json: EtherscanTxCountResponse = await response.json() - const data: TimestampedData[] = json.result - .map(({ unixTimeStamp, transactionCount }) => ({ - timestamp: +unixTimeStamp * 1000, // unix milliseconds - value: transactionCount, - })) - .sort((a, b) => a.timestamp - b.timestamp) - const { value } = data[data.length - 1] - - return { - data, // historical data: { timestamp: unix-milliseconds, value } - value, // current value (number, unformatted) - } - } catch (error: unknown) { - console.error((error as Error).message) - return { - error: (error as Error).message, - } - } -} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 63fd3212299..df5f68b4176 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -4,6 +4,8 @@ import { NavSectionKey } from "@/components/Nav/types" import i18nConfig from "../../i18n.config.json" +import type { CommunityBlog } from "./types" + export const OLD_CONTENT_DIR = "src/content" export const CONTENT_DIR = "public/content" export const TRANSLATIONS_DIR = "public/content/translations" @@ -25,6 +27,8 @@ export const LOCALES_CODES = BUILD_LOCALES // Site urls export const SITE_URL = "https://ethereum.org" export const DISCORD_PATH = "/discord/" +export const GITHUB_REPO_URL = + "https://github.com/ethereum/ethereum-org-website" export const EDIT_CONTENT_URL = `https://github.com/ethereum/ethereum-org-website/tree/dev/` export const MAIN_CONTENT_ID = "main-content" export const WEBSITE_EMAIL = "website@ethereum.org" @@ -71,8 +75,7 @@ export const REGULAR_RATES: ReportsModel.RegularRate[] = [ export const languagePathRootRegExp = /^.+\/content\/translations\/[a-z-]*\// // Metrics -export const DAYS_TO_FETCH = 90 -export const RANGES = ["30d", "90d"] as const +export const DAYS_TO_FETCH = 1 export const BEACONCHA_IN_URL = "https://beaconcha.in/" export const ETHERSCAN_API_URL = "https://api.etherscan.io" export const DUNE_API_URL = "https://api.dune.com" @@ -156,3 +159,59 @@ export const DESKTOP_LANGUAGE_BUTTON_NAME = "desktop-language-button" // Codeblock export const LINES_BEFORE_COLLAPSABLE = 8 + +// Ethereum.org community +export const CALENDAR_DISPLAY_COUNT = 4 + +// RSS Feeds +export const RSS_DISPLAY_COUNT = 6 + +export const VITALIK_FEED = "https://vitalik.eth.limo/feed.xml" +export const SOLIDITY_FEED = "https://soliditylang.org/feed.xml" +export const _0X_PARC_FEED = "https://rss.app/feeds/cWXGYts0ZM8C3F6t.xml" + +export const COMMUNITY_BLOGS: CommunityBlog[] = [ + { + href: "https://vitalik.eth.limo/", + feed: VITALIK_FEED, + }, + { + href: "https://blog.ethereum.org/", + feed: "https://blog.ethereum.org/en/feed.xml", + }, + { + href: "https://ethpandaops.io/posts/", + feed: "https://ethpandaops.io/posts/index.xml", + }, + { + href: "https://ethstaker.cc/blog", + feed: "https://paragraph.xyz/api/blogs/rss/@ethstaker", + }, + { + href: "https://0xparc.org/blog", + feed: _0X_PARC_FEED, + }, + { + href: "https://www.attestant.io/posts/", + feed: "https://www.attestant.io/posts/", + }, + { name: "Devcon", href: "https://devcon.org/en/blogs/" }, + { + href: "https://soliditylang.org/blog/", + feed: SOLIDITY_FEED, + }, + { + href: "https://mirror.xyz/privacy-scaling-explorations.eth", + feed: "https://mirror.xyz/privacy-scaling-explorations.eth/feed/atom", + }, + { + href: "https://stark.mirror.xyz/", + feed: "https://stark.mirror.xyz/feed/atom", + }, +] + +export const BLOG_FEEDS = COMMUNITY_BLOGS.map(({ feed }) => feed).filter( + Boolean +) as string[] + +export const BLOGS_WITHOUT_FEED = COMMUNITY_BLOGS.filter((item) => !item.feed) diff --git a/src/lib/types.ts b/src/lib/types.ts index b7faea71248..852fdd3cc9a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,10 +1,10 @@ import type { Options } from "mdast-util-toc" import type { NextPage } from "next" import type { AppProps } from "next/app" -import { StaticImageData } from "next/image" -import { SSRConfig } from "next-i18next" +import type { StaticImageData } from "next/image" +import type { SSRConfig } from "next-i18next" import type { ReactElement, ReactNode } from "react" -import { Icon } from "@chakra-ui/react" +import type { Icon } from "@chakra-ui/react" import type { DocsFrontmatter, @@ -18,7 +18,7 @@ import type { import type { BreadcrumbsProps } from "@/components/Breadcrumbs" import type { CallToActionProps } from "@/components/Hero/CallToAction" -import { SimulatorNav } from "@/components/Simulator/interfaces" +import type { SimulatorNav } from "@/components/Simulator/interfaces" import allQuizData from "@/data/quizzes" import allQuestionData from "@/data/quizzes/questionBank" @@ -26,12 +26,15 @@ import allQuestionData from "@/data/quizzes/questionBank" import { WALLETS_FILTERS_DEFAULT } from "./constants" import { layoutMapping } from "@/pages/[...slug]" +import twConfig from "@/styles/config" // Credit: https://stackoverflow.com/a/52331580 export type Unpacked = T extends (infer U)[] ? U : T export type ChildOnlyProp = { children?: ReactNode } +export type ClassNameProp = { className?: string } + export type NextPageWithLayout

, IP = P> = NextPage< P, IP @@ -508,19 +511,9 @@ export type StakingStatsData = { apr: number } -export type TimestampedData = { - timestamp: number - value: T -} - -export type MetricDataValue = - | { - error: string - } - | { - data: Data - value: Value - } +export type ValueOrError = + | { value: T; timestamp?: number } + | { error: string } export type EtherscanNodeResponse = { result: { @@ -545,27 +538,26 @@ export type DefiLlamaTVLResponse = { totalLiquidityUSD: number }[] -export type MetricReturnData = MetricDataValue< - TimestampedData[], - number -> +export type MetricReturnData = ValueOrError + +export type StatsBoxState = ValueOrError -export type StatsBoxState = MetricDataValue[], string> +export type GrowThePieMetricKey = "txCount" | "txCostsMedianUsd" -export type MetricSection = +export type GrowThePieData = Record + +export type MetricName = + | "ethPrice" // Use with `totalEthStaked` to convert ETH to USD | "totalEthStaked" - | "nodeCount" | "totalValueLocked" - | "txCount" + | GrowThePieMetricKey -export type AllMetricData = Record +export type AllMetricData = Record export type StatsBoxMetric = { - title: string - description: string + label: string + description?: string state: StatsBoxState - buttonContainer: JSX.Element - range: string apiUrl: string apiProvider: string } @@ -760,3 +752,110 @@ export type GHLabel = { name: string color: string } + +/** + * RSS Feed handling + */ +export type RSSItem = { + pubDate: string + title: string + source: string + link: string + sourceFeedUrl: string + sourceUrl: string + imgSrc?: string +} + +export type RSSChannel = { + title: string[] + link: string[] + description: string[] + lastBuildDate: string[] + docs: string[] + generator: string[] + image: { + url: string[] + title: string[] + link: string[] + }[] + copyright: string[] + item: { + title: string[] + link: string[] + guid: string[] + pubDate: string[] + description: string[] + category: string[] + enclosure: { + $: { + url: string[] + length: string[] + type: string[] + } + }[] + "media:content": { $: { url: string } }[] + }[] +} + +export type RSSResult = { + rss: { + channel: RSSChannel[] + } +} + +export type AtomElement = + | string + | { + _?: string // children + $: { + href?: string + } + } +export type AtomEntry = { + id: string[] + title: AtomElement[] + updated: string[] + content?: AtomElement[] + link?: AtomElement[] + summary?: AtomElement[] +} + +export type AtomResult = { + feed: { + id: string[] + title: string[] + updated: string[] + generator: string[] + link: string[] + subtitle: string[] + icon?: string[] + entry: AtomEntry[] + } +} + +export type CommunityBlog = { + href: string +} & ({ name: string; feed?: string } | { name?: string; feed: string }) + +type NestedDivs = { + div: NestedDivs[] +} + +export type HTMLResult = { + html: { + body: Record[] + } +} + +export type EventCardProps = { + title: string + href: string + startDate: string + endDate: string + description: string + className?: string + location: string + imageUrl?: string +} + +export type BreakpointKey = keyof typeof twConfig.theme.screens diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts index 54c57803232..4f8f4555f8f 100644 --- a/src/lib/utils/date.ts +++ b/src/lib/utils/date.ts @@ -1,2 +1,8 @@ export const dateToString = (published: Date | string) => new Date(published).toISOString().split("T")[0] + +export const isValidDate = (dateString?: string | number): boolean => { + if (!dateString) return false + const date = new Date(dateString) + return !isNaN(date.getTime()) +} diff --git a/src/lib/utils/rss.ts b/src/lib/utils/rss.ts new file mode 100644 index 00000000000..11de14e0f2c --- /dev/null +++ b/src/lib/utils/rss.ts @@ -0,0 +1,47 @@ +import { _0X_PARC_FEED, SOLIDITY_FEED, VITALIK_FEED } from "../constants" +import type { RSSItem } from "../types" + +export const sortByPubDate = (items: RSSItem[]) => + items.sort((a, b) => { + const dateA = new Date(a.pubDate) + const dateB = new Date(b.pubDate) + if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) { + console.error("Invalid date found:", a.pubDate, b.pubDate) + return 0 + } + return dateB.getTime() - dateA.getTime() + }) + +export const postProcess = (rssItems: RSSItem[]) => + rssItems.map((item) => { + switch (item.sourceFeedUrl) { + case VITALIK_FEED: + return { + ...item, + imgSrc: "/images/vitalik-blog-banner.svg", + link: item.link.replace(".ca", ".eth.limo"), + } + case SOLIDITY_FEED: + return { + ...item, + imgSrc: "/images/solidity-banner.png", + } + case _0X_PARC_FEED: + return { + ...item, + imgSrc: "/images/0xparc-logo.svg", + } + default: + return item + } + }) + +export const polishRSSList = (...items: RSSItem[][]) => { + const latestOfEach = items + .filter(({ length }) => length) + .map((item) => item[0]) // Take only latest post (first in array) from each + + const latestItems = latestOfEach.flat() + const readyForSorting = postProcess(latestItems) + return sortByPubDate(readyForSorting) +} diff --git a/src/lib/utils/screen.ts b/src/lib/utils/screen.ts new file mode 100644 index 00000000000..3fda3f97301 --- /dev/null +++ b/src/lib/utils/screen.ts @@ -0,0 +1,12 @@ +import type { BreakpointKey } from "../types" + +import twConfig from "@/styles/config" + +export const breakpointAsNumber = Object.entries(twConfig.theme.screens).reduce( + (acc, curr) => { + const [breakpoint, value] = curr + acc[breakpoint] = +value.split("px")[0] + return acc + }, + {} +) as Record diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 9ba1aea40a2..a294aac62e2 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,76 +1,82 @@ -import { lazy, ReactNode, Suspense, useState } from "react" +import { Fragment, lazy, Suspense } from "react" import type { GetStaticProps, InferGetStaticPropsType } from "next" -import { useRouter } from "next/router" -import { useTranslation } from "next-i18next" import { serverSideTranslations } from "next-i18next/serverSideTranslations" -import { FaGithub } from "react-icons/fa" -import { - Box, - chakra, - Divider, - Flex, - FlexProps, - Heading, - HeadingProps, - Icon, - SimpleGridProps, - SkeletonText, - Stack, - useToken, -} from "@chakra-ui/react" - -import { AllMetricData, BasePageProps, ChildOnlyProp, Lang } from "@/lib/types" -import type { CodeExample, CommunityEventsReturnType } from "@/lib/interfaces" - -import ActionCard from "@/components/ActionCard" -import ButtonLink from "@/components/Buttons/ButtonLink" -import CalloutBanner from "@/components/CalloutBanner" +import { FaDiscord, FaGithub } from "react-icons/fa6" + +import type { + AllMetricData, + BasePageProps, + CommunityBlog, + Lang, + RSSItem, +} from "@/lib/types" + +import SvgButtonLink from "@/components/Buttons/SvgButtonLink" +import { ChevronNext } from "@/components/Chevron" import CodeModal from "@/components/CodeModal" -import CommunityEvents from "@/components/CommunityEvents" import HomeHero from "@/components/Hero/HomeHero" -import { Image } from "@/components/Image" -import LazyLoadComponent from "@/components/LazyLoadComponent" +import BentoCard from "@/components/Homepage/BentoCard" +import { useHome } from "@/components/Homepage/useHome" +import AngleBrackets from "@/components/icons/angle-brackets.svg" +import Calendar from "@/components/icons/calendar.svg" +import CalendarAdd from "@/components/icons/calendar-add.svg" +import { TwImage } from "@/components/Image" import MainArticle from "@/components/MainArticle" import PageMetadata from "@/components/PageMetadata" -import TitleCardList from "@/components/TitleCardList" +import Swiper from "@/components/Swiper" import { TranslatathonBanner } from "@/components/Translatathon/TranslatathonBanner" -import Translation from "@/components/Translation" - +import { ButtonLink } from "@/components/ui/buttons/Button" +import { + Card, + CardBanner, + CardContent, + CardHighlight, + CardSubTitle, + CardTitle, +} from "@/components/ui/card" +import Link from "@/components/ui/Link" +import { + Section, + SectionBanner, + SectionContent, + SectionHeader, + SectionTag, +} from "@/components/ui/section" +import { SkeletonLines } from "@/components/ui/skeleton" +import WindowBox from "@/components/WindowBox" + +import { cn } from "@/lib/utils/cn" +import { isValidDate } from "@/lib/utils/date" import { existsNamespace } from "@/lib/utils/existsNamespace" import { getLastDeployDate } from "@/lib/utils/getLastDeployDate" +import { polishRSSList } from "@/lib/utils/rss" import { runOnlyOnce } from "@/lib/utils/runOnlyOnce" +import { breakpointAsNumber } from "@/lib/utils/screen" import { getLocaleTimestamp } from "@/lib/utils/time" -import { - getRequiredNamespacesForPage, - isLangRightToLeft, -} from "@/lib/utils/translations" +import { getRequiredNamespacesForPage } from "@/lib/utils/translations" -import CreateWalletContent from "@/data/CreateWallet" - -import { BASE_TIME_UNIT } from "@/lib/constants" +import { + BASE_TIME_UNIT, + BLOG_FEEDS, + BLOGS_WITHOUT_FEED, + CALENDAR_DISPLAY_COUNT, + GITHUB_REPO_URL, + RSS_DISPLAY_COUNT, +} from "@/lib/constants" -import SimpleDomainRegistryContent from "!!raw-loader!@/data/SimpleDomainRegistry.sol" -import SimpleTokenContent from "!!raw-loader!@/data/SimpleToken.sol" -import SimpleWalletContent from "!!raw-loader!@/data/SimpleWallet.sol" import { fetchCommunityEvents } from "@/lib/api/calendarEvents" -import { fetchNodes } from "@/lib/api/fetchNodes" +import { fetchEthPrice } from "@/lib/api/fetchEthPrice" +import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie" +import { fetchAttestantPosts } from "@/lib/api/fetchPosts" +import { fetchRSS } from "@/lib/api/fetchRSS" import { fetchTotalEthStaked } from "@/lib/api/fetchTotalEthStaked" import { fetchTotalValueLocked } from "@/lib/api/fetchTotalValueLocked" -import { fetchTxCount } from "@/lib/api/fetchTxCount" -import devfixed from "@/public/images/developers-eth-blocks.png" -import dogefixed from "@/public/images/doge-computer.png" -import enterprise from "@/public/images/enterprise-eth.png" -import ethfixed from "@/public/images/eth.png" -import finance from "@/public/images/finance_transparent.png" -import future from "@/public/images/future_transparent.png" -import hackathon from "@/public/images/hackathon_transparent.png" -import hero from "@/public/images/home/hero.png" -import impact from "@/public/images/impact_transparent.png" -import infrastructure from "@/public/images/infrastructure_transparent.png" -import infrastructurefixed from "@/public/images/infrastructure_transparent.png" -import merge from "@/public/images/upgrades/merge.png" -import robotfixed from "@/public/images/wallet-cropped.png" -import ethereum from "@/public/images/what-is-ethereum.png" +import EventFallback from "@/public/images/events/event-placeholder.png" +import BuildersImage from "@/public/images/heroes/developers-hub-hero.jpg" +import ActivityImage from "@/public/images/heroes/layer-2-hub-hero.jpg" +import LearnImage from "@/public/images/heroes/learn-hub-hero.png" +import CommunityImage from "@/public/images/heroes/quizzes-hub-hero.png" +import Hero from "@/public/images/home/hero.png" // lazy loaded components const Codeblock = lazy(() => @@ -80,150 +86,40 @@ const Codeblock = lazy(() => new Promise((resolve) => setTimeout(resolve, 1000)), ]).then(([module]) => module) ) -const StatsBoxGrid = lazy(() => import("@/components/StatsBoxGrid")) - -const Skeleton = () => ( - - - -) - -const SectionHeading = (props: HeadingProps) => ( - -) - -const SectionDecription = (props: ChildOnlyProp) => ( - -) - -const ImageContainer = (props: FlexProps & { children: ReactNode }) => ( - -) - -const CardContainer = (props: { - children: ReactNode - minChildWidth: SimpleGridProps["minChildWidth"] -}) => ( - *": { - minW: props.minChildWidth, - }, - }} - > - {props.children} - -) - -const ContentBox = (props: ChildOnlyProp) => ( - -) - -const StyledActionCard = chakra(ActionCard, { - baseStyle: { - background: "background.base", - borderRadius: "sm", - border: "1px", - borderColor: "text", - margin: 0, - }, -}) - -const StyledTitleCardList = chakra(TitleCardList) - -const GrayContainer = (props: ChildOnlyProp) => ( - -) -const MainSectionContainer = (props: { - children: ReactNode - containerBg: FlexProps["bg"] -}) => ( - - {props.children} - -) - -const FeatureContent = (props: ChildOnlyProp) => ( - -) - -const Row = (props: { children: ReactNode; isReversed?: boolean }) => ( - - {props.children} - -) - -const ButtonLinkRow = (props: ChildOnlyProp) => ( - -) +const StatsBoxGrid = lazy(() => import("@/components/StatsBoxGrid")) -const cachedFetchCommunityEvents = runOnlyOnce(fetchCommunityEvents) +const cachedEthPrice = runOnlyOnce(fetchEthPrice) const cachedFetchTotalEthStaked = runOnlyOnce(fetchTotalEthStaked) -const cachedFetchNodes = runOnlyOnce(fetchNodes) const cachedFetchTotalValueLocked = runOnlyOnce(fetchTotalValueLocked) -const cachedFetchTxCount = runOnlyOnce(fetchTxCount) +const cachedXmlBlogFeeds = runOnlyOnce(async () => await fetchRSS(BLOG_FEEDS)) +const cachedAttestantBlog = runOnlyOnce(fetchAttestantPosts) +const cachedGrowThePieData = runOnlyOnce(fetchGrowThePie) +const cachedFetchCommunityEvents = runOnlyOnce(fetchCommunityEvents) type Props = BasePageProps & { - communityEvents: CommunityEventsReturnType metricResults: AllMetricData + rssData: { rssItems: RSSItem[]; blogLinks: CommunityBlog[] } } export const getStaticProps = (async ({ locale }) => { + const growThePieData = await cachedGrowThePieData() const metricResults: AllMetricData = { + ethPrice: await cachedEthPrice(), totalEthStaked: await cachedFetchTotalEthStaked(), - nodeCount: await cachedFetchNodes(), totalValueLocked: await cachedFetchTotalValueLocked(), - txCount: await cachedFetchTxCount(), + txCount: growThePieData.txCount, + txCostsMedianUsd: growThePieData.txCostsMedianUsd, } const communityEvents = await cachedFetchCommunityEvents() + const calendar = communityEvents.upcomingEventData + .sort((a, b) => { + const dateA = isValidDate(a.date) ? new Date(a.date).getTime() : -Infinity + const dateB = isValidDate(b.date) ? new Date(b.date).getTime() : -Infinity + return dateA - dateB + }) + .slice(0, CALENDAR_DISPLAY_COUNT) // load i18n required namespaces for the given page const requiredNamespaces = getRequiredNamespacesForPage("/") @@ -238,422 +134,534 @@ export const getStaticProps = (async ({ locale }) => { lastDeployDate ) + // load RSS feed items + const xmlBlogs = await cachedXmlBlogFeeds() + const attestantBlog = await cachedAttestantBlog() + const polishedRssItems = polishRSSList(attestantBlog, ...xmlBlogs) + const rssItems = polishedRssItems.slice(0, RSS_DISPLAY_COUNT) + + const blogLinks = polishedRssItems.map(({ source, sourceUrl }) => ({ + name: source, + href: sourceUrl, + })) as CommunityBlog[] + blogLinks.push(...BLOGS_WITHOUT_FEED) + return { props: { ...(await serverSideTranslations(locale!, requiredNamespaces)), - communityEvents, + calendar, contentNotTranslated, lastDeployLocaleTimestamp, metricResults, + rssData: { rssItems, blogLinks }, }, revalidate: BASE_TIME_UNIT * 24, } }) satisfies GetStaticProps const HomePage = ({ - communityEvents, + calendar, metricResults, + rssData: { rssItems, blogLinks }, }: InferGetStaticPropsType) => { - const { t } = useTranslation(["common", "page-index"]) - const { locale, asPath } = useRouter() - const [isModalOpen, setModalOpen] = useState(false) - const [activeCode, setActiveCode] = useState(0) - const dir = isLangRightToLeft(locale as Lang) ? "rtl" : "ltr" - - const toggleCodeExample = (id: number): void => { - setActiveCode(id) - setModalOpen(true) - } - - const cards = [ - { - image: robotfixed, - title: t("page-index:page-index-get-started-wallet-title"), - description: t("page-index:page-index-get-started-wallet-description"), - alt: t("page-index:page-index-get-started-wallet-image-alt"), - href: "/wallets/find-wallet/", - }, - { - image: ethfixed, - title: t("page-index:page-index-get-started-eth-title"), - description: t("page-index:page-index-get-started-eth-description"), - alt: t("page-index:page-index-get-started-eth-image-alt"), - href: "/get-eth/", - }, - { - image: dogefixed, - title: t("page-index:page-index-get-started-dapps-title"), - description: t("page-index:page-index-get-started-dapps-description"), - alt: t("page-index:page-index-get-started-dapps-image-alt"), - href: "/dapps/", - }, - { - image: devfixed, - title: t("page-index:page-index-get-started-devs-title"), - description: t("page-index:page-index-get-started-devs-description"), - alt: t("page-index:page-index-get-started-devs-image-alt"), - href: "/developers/", - }, - ] - - const touts = [ - { - image: merge, - alt: t("page-index:page-index-tout-upgrades-image-alt"), - title: t("page-index:page-index-tout-upgrades-title"), - description: t("page-index:page-index-tout-upgrades-description"), - href: "/roadmap/", - }, - { - image: infrastructurefixed, - alt: t("page-index:page-index-tout-enterprise-image-alt"), - title: t("page-index:page-index-tout-enterprise-title"), - description: t("page-index:page-index-tout-enterprise-description"), - href: "/enterprise/", - }, - { - image: enterprise, - alt: t("page-index:page-index-tout-community-image-alt"), - title: t("page-index:page-index-tout-community-title"), - description: t("page-index:page-index-tout-community-description"), - href: "/community/", - }, - ] - - const codeExamples: Array = [ - { - title: t("page-index:page-index-developers-code-example-title-0"), - description: t( - "page-index:page-index-developers-code-example-description-0" - ), - codeLanguage: "language-solidity", - code: SimpleWalletContent, - }, - { - title: t("page-index:page-index-developers-code-example-title-1"), - description: t( - "page-index:page-index-developers-code-example-description-1" - ), - codeLanguage: "language-solidity", - code: SimpleTokenContent, - }, - { - title: t("page-index:page-index-developers-code-example-title-2"), - description: t( - "page-index:page-index-developers-code-example-description-2" - ), - codeLanguage: "language-javascript", - code: CreateWalletContent, - }, - { - title: t("page-index:page-index-developers-code-example-title-3"), - description: t( - "page-index:page-index-developers-code-example-description-3" - ), - codeLanguage: "language-solidity", - code: SimpleDomainRegistryContent, - }, - ] - - const cardBoxShadow = useToken("colors", "cardBoxShadow") + const { + t, + locale, + asPath, + dir, + isModalOpen, + setModalOpen, + activeCode, + toggleCodeExample, + codeExamples, + subHeroCTAs, + popularTopics, + upcomingEvents, + joinActions, + bentoItems, + } = useHome() return ( - + - - - - {/* Getting Started Section */} - - - +

+
+ {subHeroCTAs.map(({ label, description, href, className, Svg }) => ( + + +

{description}

+
+ +

{description}

+
+
+ ))} +
+ + {/* Use Cases - A new way to use the internet */} +
+
- - - - - - - - - - {t("page-index:page-index-get-started-image-alt")} - - - - {cards.map((card, idx) => ( - + {t("common:nav-use-cases-label")} +
+

+ {t("page-index:page-index-bento-header")} +

+
+ + {/* Mobile */} + + {bentoItems.map(({ className, ...item }) => ( + ))} - - - - {/* What is Eth Section */} - - - - - - - - - - - - - - - - - - - - {t("page-index:page-index-what-is-ethereum-image-alt")} + + {/* Desktop */} + {bentoItems.map(({ className, ...item }) => ( + - - - - {/* Finance Section */} - - - - - - - - - - - - + ))} +

+ + {/* Activity - The strongest ecosystem */} +
+ + + + + + {t("page-index:page-index-activity-tag")} + + {t("page-index:page-index-activity-header")} + +
+

+ {t("page-index:page-index-activity-description")} +

+ }> + + +
+
+
+ + {/* Learn - Understand Ethereum */} +
+ + + + + + {t("page-index:page-index-learn-tag")} + + {t("page-index:page-index-learn-header")} + +
+

+ {t("page-index:page-index-learn-description")} +

+
+

+ {t("page-index:page-index-popular-topics-header")} +

+
+ {popularTopics.map(({ label, Svg, href }) => ( + +

+ {label} +

+
+ ))} +
+
+ + {t("page-index:page-index-popular-topics-action")}{" "} + + +
+
+
{" "} +
+
+ + {/* TODO: Add "The Internet Is Changing" section */} + + {/* Builders - Blockchain's biggest builder community */} +
+ + + + + + {t("page-index:page-index-builders-tag")} + + {t("page-index:page-index-builders-header")} + +

+ {t("page-index:page-index-builders-description")} +

+
+ + {t("page-index:page-index-builders-action-primary")}{" "} + - - - - {t("page-index:page-index-defi-image-alt")} - - - - {/* NFT Section */} - - - - - - - - - - - - + + {t("page-index:page-index-builders-action-secondary")} - - - - {t("page-index:page-index-nft-alt")} - - - - {/* Internet Section */} - - - - - - - - - - - - - - - - - - - - - {t("page-index:page-index-internet-image-alt")} - - - - - {/* Developer Section */} - - - - - - - - - - - - - - - +
+
+ + {codeExamples.map(({ title, description }, idx) => ( + + ))} + +
+ + {isModalOpen && ( + // TODO: Migrate CodeModal, CodeBlock from Chakra-UI to tailwind/shad-cn + + }> + + {codeExamples[activeCode].code} + + + + )} +
+
+ + {/* Ethereum.org community - Built by the community */} +
+ + + + + + {t("page-index:page-index-community-tag")} + + {t("page-index:page-index-community-header")} + +
+

{t("page-index:page-index-community-description-1")}

+

{t("page-index:page-index-community-description-2")}

+

{t("page-index:page-index-community-description-3")}

+
+
+ + {t("page-index:page-index-community-action")} - - - {/* Render CodeModal & Codeblock conditionally */} - {isModalOpen && ( - - }> - + - {codeExamples[activeCode].code} - - - + + + + + +
+
+
+ + {calendar.length > 0 ? ( + calendar.map(({ date, title, calendarLink }) => ( +
+
+ + {title} + +

+ {new Intl.DateTimeFormat(locale, { + month: "long", + day: "2-digit", + year: "numeric", + hour: "numeric", + minute: "numeric", + }).format(new Date(date))} +

+
+ + {" "} + {t("page-index:page-index-calendar-add")} + +
+ )) + ) : ( +
+ {t("page-index:page-index-calendar-fallback")} +
+ )} +
+
{" "} + + + + {/* Recent posts */} +
+

+ {t("page-index:page-index-posts-header")} +

+

{t("page-index:page-index-posts-subtitle")}

+ + + {rssItems.map(({ pubDate, title, source, link, imgSrc }) => ( + + + {/* eslint-disable-next-line @next/next/no-img-element */} + + + + {title} + {isValidDate(pubDate) && ( + + {new Intl.DateTimeFormat(locale, { + month: "long", + day: "numeric", + year: "numeric", + }).format(new Date(pubDate))} + + )} + {source} + + + ))} + + +
+

{t("page-index:page-index-posts-action")}

+
+ {blogLinks.map(({ name, href }) => ( + + {name} + + ))} +
+
+
+ + {/* Events */} +
+

+ {t("page-index:page-index-events-header")} +

+

{t("page-index:page-index-events-subtitle")}

+
+
+ {upcomingEvents.map( + ( + { + title, + href, + location, + description, + startDate, + endDate, + imageUrl, + }, + idx + ) => ( + + + {imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( + + )} + + + {title} + + {(isValidDate(startDate) || isValidDate(endDate)) && + new Intl.DateTimeFormat(locale, { + month: "long", + day: "numeric", + year: "numeric", + }).formatRange( + new Date( + isValidDate(startDate) ? startDate : endDate + ), + new Date(isValidDate(endDate) ? endDate : startDate) + )} + + {location} + + + ) + )} +
+
+
+ + {t("page-index:page-index-events-action")} + +
+
+ + {/* Join ethereum.org */} +
- - {/* Eth Today Section */} - - - - - - - - - - - } - componentProps={{ data: metricResults }} - intersectionOptions={{ - root: null, - rootMargin: "500px", - threshold: 0, - }} - /> - - - - {/* Explore Section */} - - - - - - - - {touts.map((tout, idx) => { - return ( - - ) - })} - - - - - - - } - variant="outline" - isSecondary - > - GitHub - - - - - +
+
+

{t("page-index:page-index-join-header")}

+

{t("page-index:page-index-join-description")}

+
+
+ {joinActions.map( + ({ Svg, label, href, className, description }) => ( + +

{description}

+
+ ) + )} +
+
+
+
+ ) } diff --git a/src/pages/what-is-ethereum.tsx b/src/pages/what-is-ethereum.tsx index a03639b3e87..3b8bb28d605 100644 --- a/src/pages/what-is-ethereum.tsx +++ b/src/pages/what-is-ethereum.tsx @@ -49,7 +49,7 @@ import { getRequiredNamespacesForPage, } from "@/lib/utils/translations" -import { fetchTxCount } from "@/lib/api/fetchTxCount" +import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie" import dogeComputerImg from "@/public/images/doge-computer.png" import ethImg from "@/public/images/eth.png" import diffEthAndBtc from "@/public/images/eth.png" @@ -157,7 +157,7 @@ const Image400 = ({ src }: Pick) => ( ) -const cachedFetchTxCount = runOnlyOnce(fetchTxCount) +const cachedFetchTxCount = runOnlyOnce(fetchGrowThePie) type Props = BasePageProps & { data: MetricReturnData @@ -181,7 +181,7 @@ export const getStaticProps = (async ({ locale }) => { ...(await serverSideTranslations(locale!, requiredNamespaces)), contentNotTranslated, lastDeployLocaleTimestamp, - data, + data: data.txCount, }, } }) satisfies GetStaticProps diff --git a/src/styles/colors.css b/src/styles/colors.css new file mode 100644 index 00000000000..46942217230 --- /dev/null +++ b/src/styles/colors.css @@ -0,0 +1,87 @@ +@tailwind base; + +/* FOUNDATIONAL COLOR PALETTE DECLARATIONS */ + +@layer base { + :root { + --white: 0, 0%, 100%; /* #ffffff */ + --gray-50: 0, 0%, 97%; /* #f7f7f7 */ + --gray-100: 0, 0%, 93%; /* #eeeeee */ + --gray-150: 0, 0%, 93%; /* #ececec */ /* TODO: Confirm this shade, used in nav menu */ + --gray-200: 0, 0%, 81%; /*#cecece */ + --gray-300: 0, 0%, 67%; /* #acacac */ + --gray-400: 0, 0%, 55%; /* #8C8C8C */ + --gray-500: 0, 0%, 38%; /* #616161 */ + --gray-600: 0, 0%, 20%; /* #333333 */ + --gray-700: 0, 0%, 13%; /* #222222 */ + --gray-800: 0, 0%, 11%; /* #1b1b1b */ + --gray-900: 0, 0%, 7%; /* #121212 */ + --gray-950: 0, 0%, 4%; /* #0a0a0a */ + --black: 0, 0%, 0%; /* #000000 */ + + --purple-50: 262, 100%, 96%; /* #F3ECFF */ + --purple-100: 263, 100%, 94%; /* #EDE2FF */ + --purple-200: 263, 91%, 88%; /* #DAC5FC */ + --purple-300: 263, 94%, 84%; /* #CCAFFC */ + --purple-400: 263, 77%, 75%; /* #B38DF0 */ + --purple-500: 263, 88%, 65%; /* #945AF4 */ + --purple-600: 263, 75%, 51%; /* #6C24DF */ + --purple-700: 263, 74%, 41%; /* #561BB5 */ + --purple-800: 263, 77%, 31%; /* #41128B */ + --purple-900: 263, 86%, 15%; /* #1E0546 */ + + --pink-50: 325, 63%, 93%; /* #F8E0EE */ + --pink-100: 322, 78%, 87%; /* #F8C5E5 */ + --pink-200: 323, 100%, 85%; /* #FFB2E2 */ + --pink-300: 323, 100%, 83%; /* #FFA6DD */ + --pink-400: 323, 91%, 75%; /* #F986CD */ + --pink-500: 323, 100%, 66%; /* #FF51BC */ + --pink-600: 323, 93%, 51%; /* #F6109E */ + --pink-700: 323, 99%, 39%; /* #C7017B */ + --pink-800: 323, 87%, 29%; /* #8C0A5A */ + --pink-900: 323, 82%, 18%; /* #530836 */ + + --blue-50: 214, 100%, 99%; /* #F8FBFF */ + --blue-100: 217, 100%, 95%; /* #E8F1FF */ + --blue-200: 214, 86%, 89%; /* #CADFFB */ + --blue-300: 221, 79%, 74%; /* #88AAF1 */ + --blue-400: 221, 90%, 69%; /* #6995F7 */ + --blue-500: 224, 84%, 60%; /* #4473EF */ + --blue-600: 235, 81%, 58%; /* #3C4CEB */ + --blue-700: 235, 59%, 41%; /* #2B36A8 */ + --blue-800: 231, 53%, 29%; /* #232F71 */ + --blue-900: 217, 36%, 17%; /* #1B273A */ + + --teal-50: 164, 100%, 98%; /* #F4FFFC */ + --teal-100: 164, 79%, 95%; /* #E6FCF6 */ + --teal-200: 163, 81%, 85%; /* #BBF8E7 */ + --teal-300: 163, 96%, 78%; /* #91FDDE */ + --teal-400: 163, 90%, 65%; /* #58F6C9 */ + --teal-500: 163, 76%, 48%; /* #1DD8A3 */ + --teal-600: 163, 88%, 39%; /* #0CB988 */ + --teal-700: 163, 82%, 33%; /* #0F9971 */ + --teal-800: 163, 93%, 21%; /* #04674B */ + --teal-900: 162, 97%, 13%; /* #01422F */ + + --orange-100: 30, 100%, 94%; /* #FFF0DB */ + --orange-200: 30, 100%, 82%; /* #FFD7A7 */ + --orange-300: 30, 98%, 70%; /* #FEB077 */ + --orange-400: 30, 97%, 58%; /* #FD8640 */ + --orange-500: 30, 95%, 51%; /* #FB610E */ + --orange-600: 20, 95%, 47%; /* #EC4A0A */ + --orange-700: 15, 90%, 39%; /* #C4350A */ + --orange-800: 10, 76%, 28%; /* #7D2711 */ + --orange-900: 20, 33%, 15%; /* #3A291D */ + + /* TODO: Update to new color theming */ + --red-100: 0, 75%, 88%; /* #f7c8c8 */ + --red-500: 0, 100%, 36%; /* #b80000 */ + --red-800: 0, 33%, 7%; /* #180c0c */ + --green-100: 138, 51%, 91%; /* #ddf4e4 */ + --green-500: 155, 84%, 24%; /* #0a7146 */ + --green-900: 140, 37%, 6%; /* #0a160e */ + --yellow-200: 47, 100%, 94%; /* #fff8df */ + --yellow-500: 42, 100%, 37%; /* #bd8400 */ + + } +} diff --git a/src/styles/config.ts b/src/styles/config.ts new file mode 100644 index 00000000000..8ce8205b09d --- /dev/null +++ b/src/styles/config.ts @@ -0,0 +1,7 @@ +import resolveConfig from "tailwindcss/resolveConfig" + +import tailwindConfig from "../../tailwind.config" + +const config = resolveConfig(tailwindConfig) + +export default config diff --git a/src/styles/docsearch.css b/src/styles/docsearch.css index dfce14f0ccb..91f6906a80d 100644 --- a/src/styles/docsearch.css +++ b/src/styles/docsearch.css @@ -38,7 +38,8 @@ --docsearch-modal-width: 650px; --docsearch-hit-height: fit-content; } -html[data-theme="dark"] { + +.dark { --docsearch-modal-background: theme(backgroundColor.background.DEFAULT); --docsearch-highlight-color: theme(colors.primary.hover); } diff --git a/src/styles/fonts.css b/src/styles/fonts.css index d9e62abd6a8..213b5fbc7fd 100644 --- a/src/styles/fonts.css +++ b/src/styles/fonts.css @@ -1,4 +1,4 @@ -/* css imported from https://fonts.googleapis.com/css2?family=Inter:wght@400;700 */ +/* css imported from https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900 */ /* cyrillic-ext */ @font-face { @@ -9,7 +9,7 @@ src: url(/fonts/inter/cyrillic-ext.woff2) format("woff2"); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; - } +} /* cyrillic */ @font-face { font-family: "Inter"; @@ -36,7 +36,7 @@ font-display: swap; src: url(/fonts/inter/greek.woff2) format("woff2"); unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, - U+03A3-03FF; + U+03A3-03FF; } /* vietnamese */ @font-face { @@ -46,7 +46,7 @@ font-display: swap; src: url(/fonts/inter/vietnamese.woff2) format("woff2"); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, - U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, + U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -58,7 +58,7 @@ src: url(/fonts/inter/latin-ext.woff2) format("woff2"); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; - } +} /* latin */ @font-face { font-family: "Inter"; @@ -67,9 +67,10 @@ font-display: swap; src: url(/fonts/inter/latin.woff2) format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, - U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, - U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, + U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } + /* cyrillic-ext */ @font-face { font-family: "Inter"; @@ -78,7 +79,7 @@ font-display: swap; src: url(/fonts/inter/cyrillic-ext.woff2) format("woff2"); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, - U+FE2E-FE2F; + U+FE2E-FE2F; } /* cyrillic */ @font-face { @@ -106,7 +107,7 @@ font-display: swap; src: url(/fonts/inter/greek.woff2) format("woff2"); unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, - U+03A3-03FF; + U+03A3-03FF; } /* vietnamese */ @font-face { @@ -118,8 +119,8 @@ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; - } - /* latin-ext */ +} +/* latin-ext */ @font-face { font-family: "Inter"; font-style: normal; @@ -127,7 +128,7 @@ font-display: swap; src: url(/fonts/inter/latin-ext.woff2) format("woff2"); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, - U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { @@ -137,49 +138,119 @@ font-display: swap; src: url(/fonts/inter/latin.woff2) format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, - U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, - U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, + U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* cyrillic-ext */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/cyrillic-ext.woff2) format("woff2"); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/cyrillic.woff2) format("woff2"); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/greek-ext.woff2) format("woff2"); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/greek.woff2) format("woff2"); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, + U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/vietnamese.woff2) format("woff2"); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, + U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/latin-ext.woff2) format("woff2"); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: url(/fonts/inter/latin.woff2) format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, + U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* css imported from https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400 */ /* cyrillic-ext */ @font-face { - font-family: 'IBM Plex Mono'; + font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; - src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format('truetype'); - unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; + src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format("truetype"); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; } /* cyrillic */ @font-face { - font-family: 'IBM Plex Mono'; + font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; - src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format('truetype'); + src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format("truetype"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @font-face { - font-family: 'IBM Plex Mono'; + font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; - src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format('truetype'); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; + src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format("truetype"); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, + U+1EA0-1EF9, U+20AB; } /* latin-ext */ @font-face { - font-family: 'IBM Plex Mono'; + font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; - src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format('truetype'); - unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format("truetype"); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { - font-family: 'IBM Plex Mono'; + font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; - src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format('truetype'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} \ No newline at end of file + src: url(/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf) format("truetype"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, + U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/src/styles/global.css b/src/styles/global.css index 5a345a2b858..2a7cc970e30 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -3,6 +3,8 @@ @tailwind utilities; @import "@docsearch/css"; +@import "@/styles/colors.css"; +@import "@/styles/semantic-tokens.css"; @import "@/styles/fonts.css"; @import "@/styles/docsearch.css"; @@ -11,82 +13,13 @@ --font-inter: Inter, sans-serif; --font-mono: "IBM Plex Mono", Courier, monospace; - /* Primitive Color Scheme */ - --gray-100: #f7f7f7; - --gray-150: #f2f2f2; - --gray-200: #e7e7e7; - --gray-300: #c8c8c8; - --gray-400: #8c8c8c; - --gray-500: #616161; - --gray-600: #333333; - --gray-700: #222222; - --gray-800: #1b1b1b; - --gray-900: #141414; - - --blue-50: #f6f6ff; - --blue-100: #ebebff; - --blue-200: #d6d6ff; - --blue-300: #9999ff; - --blue-400: #5555ff; - --blue-500: #1c1cff; - --blue-600: #000066; - --blue-700: #0000a3; - --blue-800: #000066; - --blue-900: #000029; - - --orange-50: #fff3ed; - --orange-100: #ffe5d6; - --orange-200: #ffcbad; - --orange-300: #ffb18f; - --orange-400: #ff985c; - --orange-500: #ff7324; - --orange-550: #df5a0e; - --orange-600: #b84300; - --orange-700: #7a2d00; - --orange-800: #521e00; - --orange-900: #2f1000; - - --red-100: #f7c8c8; - --red-500: #b80000; - /* ! Deprecating 900 */ - --red-900: #180c0c; - - --green-100: #ddf4e4; - /* ! Deprecating 400 */ - --green-400: #48bb78; - --green-500: #0a7146; - /* ! Deprecating 900 */ - --green-900: #0a160e; - - --yellow-200: #fff8df; - --yellow-500: #bd8400; - /* Semantic Colors: Light mode */ - --primary: var(--blue-500); - --primary-high-contrast: var(--blue-800); - --primary-low-contrast: var(--blue-100); - --primary-hover: var(--blue-400); - --primary-visited: var(--blue-700); /* ! Deprecating primary-light */ --primary-light: var(--blue-100); /* ! Deprecating primary-dark */ --primary-dark: var(--blue-700); - /* ! Deprecating primary-pressed */ - --primary-pressed: var(--blue-400); - - --body: var(--gray-800); - --body-medium: var(--gray-500); - --body-light: var(--gray-200); - /* ! Deprecating body-inverted */ - --body-inverted: var(--gray-100); - - --background: white; - --background-highlight: var(--gray-100); - --disabled: var(--gray-400); - - /* ! Deprecating neutral */ - --neutral: white; + --neutral: var(--white); /* Complementary Set */ --attention: var(--yellow-500); @@ -110,16 +43,25 @@ --tooltip-shadow: rgba(0, 0, 0, 0.24); --switch-background: var(--gray-300); --hub-hero-content-bg: rgba(255, 255, 255, 0.8); - --main-gradient: linear-gradient( + --gradient-main: linear-gradient( 102.7deg, rgba(185, 185, 241, 0.2) 0%, rgba(84, 132, 234, 0.2) 51.56%, rgba(58, 142, 137, 0.2) 100% ); - --feedback-gradient: var(--main-gradient); + --feedback-gradient: var(--gradient-main); --table-box-shadow: 0 14px 66px rgba(0, 0, 0, 0.07), 0 10px 17px rgba(0, 0, 0, 0.03), 0 4px 7px rgba(0, 0, 0, 0.05); --table-item-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + + --gradient-banner: radial-gradient( + 155% 100% at 50% 0%, + rgba(201, 179, 245, 0.16) 0%, + rgba(201, 179, 245, 0.48) 33%, + rgba(136, 170, 241, 0.16) 66%, + rgba(255, 255, 255, 0) 100% + ); + --banner-grid-gradient: linear-gradient( 90deg, rgba(127, 127, 213, 0.2) 0%, @@ -129,30 +71,12 @@ --search-background: var(--background); } - [data-theme="dark"] { + .dark { /* Semantic Colors: Dark mode */ - --primary: var(--orange-500); - --primary-high-contrast: var(--orange-100); - --primary-low-contrast: var(--orange-800); - --primary-hover: var(--orange-400); - --primary-visited: var(--orange-550); /* ! Deprecating primary-light */ - --primary-light: var(--orange-100); + --primary-light: hsla(var(--orange-100)); /* ! Deprecating primary-dark */ - --primary-dark: var(--orange-800); - /* ! Deprecating primary-pressed */ - --primary-pressed: var(--orange-800); - - --body: var(--gray-100); - --body-medium: var(--gray-400); - --body-light: var(--gray-600); - /* ! Deprecating body-inverted */ - --body-inverted: var(--gray-800); - - --background: var(--gray-800); - --background-highlight: var(--gray-900); - - --disabled: var(--gray-500); + --primary-dark: hsla(var(--orange-800)); /* ! Deprecating neutral */ --neutral: var(--gray-900); @@ -174,7 +98,7 @@ --tooltip-shadow: rgba(255, 255, 255, 0.24); --switch-background: rgba(255, 255, 255, 0.24); --hub-hero-content-bg: rgba(34, 34, 34, 0.8); - --main-gradient: linear-gradient( + --gradient-main: linear-gradient( 102.7deg, rgba(185, 185, 241, 0.2) 0%, rgba(84, 132, 234, 0.2) 51.56%, @@ -200,7 +124,7 @@ @layer base { body { - @apply bg-background font-body text-sm text-body lg:text-md; + @apply !bg-background font-body text-sm !text-body lg:text-md; } a { @@ -278,3 +202,70 @@ } } } + +@layer components { + .swiper-horizontal > .swiper-pagination-bullets, + .swiper-pagination-bullets.swiper-pagination-horizontal { + @apply bg-background-highlight; + } + + .swiper-pagination.swiper-pagination-clickable.swiper-pagination-bullets.swiper-pagination-horizontal { + @apply !relative; + } + + .swiper-pagination { + @apply relative mx-auto mt-8 flex h-[26px] max-w-48 items-center justify-center rounded-full bg-background-highlight; + } + + .css-posts-swiper .swiper-pagination { + @apply max-w-52 sm:max-w-40 lg:max-w-36; + } + + .swiper-pagination .swiper-pagination-bullet { + @apply bg-primary-high-contrast; + } + + .swiper-pagination.swiper-pagination-clickable.swiper-pagination-bullets.swiper-pagination-horizontal + .swiper-pagination-bullet-active { + @apply bg-primary-hover; + } + + .swiper-button-prev, + .swiper-button-next { + @apply !h-6 !w-fit fill-primary px-2; + } +} + +.swiper-button-prev, +.swiper-button-next { + top: calc(100% - 11px) !important; + --nav-inset: calc(50% - 6.5rem); +} + +@media (min-width: theme("screens.sm")) { + .swiper-button-prev, + .swiper-button-next { + --nav-inset: calc(50% - 5rem); + } +} + +@media (min-width: theme("screens.lg")) { + .swiper-button-prev, + .swiper-button-next { + --nav-inset: calc(50% - 4.5rem); + } +} + +.swiper-button-next { + inset-inline-end: var(--nav-inset) !important; + inset-inline-start: auto !important; +} + +.swiper-button-prev { + inset-inline-end: auto !important; + inset-inline-start: var(--nav-inset) !important; +} + +.swiper-slide-shadow { + background: transparent !important; +} diff --git a/src/styles/semantic-tokens.css b/src/styles/semantic-tokens.css new file mode 100644 index 00000000000..e8400ac7236 --- /dev/null +++ b/src/styles/semantic-tokens.css @@ -0,0 +1,131 @@ +@tailwind base; + +/* SEMANTIC TOKEN DECLARATIONS: Abstracted from color palettes for theming */ + +@layer base { + /* Light mode (default) token declarations */ + :root { + --body: var(--gray-900); + --body-medium: var(--gray-500); + --body-light: var(--gray-200); + --body-inverse: var(--white); + --disabled: var(--gray-400); + --background: var(--white); + --background-highlight: var(--gray-50); + + --primary: var(--purple-600); + --primary-high-contrast: var(--purple-800); + --primary-low-contrast: var(--purple-100); + --primary-hover: var(--purple-500); + --primary-visited: var(--purple-700); + --primary-action: var(--purple-600); + --primary-action-hover: var(--purple-500); + + --accent-a: var(--blue-600); + --accent-a-hover: var(--blue-500); + + --accent-b: var(--pink-600); + --accent-b-hover: var(--pink-500); + + --accent-c: var(--teal-700); + --accent-c-hover: var(--teal-600); + + /** + /* Gradients (radial, conic) + /* For linear-gradient, Tailwind classes preferred + /* https://tailwindcss.com/docs/background-image#linear-gradients + */ + --radial-a-opacity-1: 0.08; + --radial-a-opacity-2: 0.24; + --radial-a: radial-gradient( + 127.67% 82.36% at 50% -30.36%, + hsla(var(--purple-500), var(--radial-a-opacity-1)) 0%, + hsla(var(--purple-500), var(--radial-a-opacity-2)) 33%, + hsla(var(--blue-500), var(--radial-a-opacity-1)) 66%, + transparent 100% + ); + + /* Shadows */ + --shadow-color-a: hsla(var(--purple-800), 0.02); + --shadow-color-b: hsla(var(--red-800), 0.04); + --shadow-color-c: hsla(var(--purple-700), 0.04); + --shadow-color-d: hsla(var(--purple-100), 0.08); + + --shadow-svg-button-link-1: 2px 2px 12px 1px var(--shadow-color-a); + --shadow-svg-button-link-2: 12px 16px 12px -3px var(--shadow-color-a); + --shadow-svg-button-link-3: 24px 32px 24px -6px var(--shadow-color-a); + --shadow-svg-button-link-4: 32px 40px 40px -12px var(--shadow-color-b); + --shadow-svg-button-link-1-hover: 0px 0px 12px 2px var(--shadow-color-a); + --shadow-svg-button-link-2-hover: 0px 0px 12px 2px var(--shadow-color-a); + --shadow-svg-button-link-3-hover: 0px 0px 24px 2px var(--shadow-color-a); + --shadow-svg-button-link-4-hover: 0px 0px 40px 2px var(--shadow-color-b); + + --shadow-body-color-a: hsla(var(--gray-900), 0.25); + --shadow-body-color-b: hsla(var(--gray-900), 0.05); + --shadow-body-md: 0px 4px 4px 0px var(--shadow-body-color-a); + --shadow-body-lg: 0px -6px 10px 0px var(--shadow-body-color-b); + + --shadow-window-box-1-opacity: 0.02; + --shadow-window-box-2-opacity: 0.02; + --shadow-window-box-3-opacity: 0.02; + --shadow-window-box-4-opacity: 0.04; + --shadow-window-box-5-opacity: 0.08; + --shadow-window-box-1: 0px 2px 12px 1px + hsla(var(--purple-800), var(--shadow-window-box-1-opacity)); + --shadow-window-box-2: 0px 16px 12px -3px hsla(var(--purple-800), var(--shadow-window-box-2-opacity)); + --shadow-window-box-3: 0px 32px 24px -6px hsla(var(--purple-800), var(--shadow-window-box-3-opacity)); + --shadow-window-box-4: 0px 40px 40px -12px hsla(var(--purple-700), var(--shadow-window-box-4-opacity)); + --shadow-window-box-5: 0px -64px 120px 80px hsla(var(--purple-100), var(--shadow-window-box-5-opacity)); + } + + /* Dark mode token declarations */ + .dark { + --body: var(--gray-100); + --body-medium: var(--gray-400); + --body-light: var(--gray-600); + --body-inverse: var(--black); + --disabled: var(--gray-500); + + --background: var(--black); + --background-highlight: var(--gray-900); + + --primary: var(--purple-400); + --primary-high-contrast: var(--purple-200); + --primary-low-contrast: var(--purple-900); + --primary-hover: var(--purple-300); + --primary-visited: var(--purple-500); + /* TODO: hover same as action in dark mode: */ + + --accent-a: var(--blue-400); + --accent-a-hover: var(--blue-300); + + --accent-b: var(--pink-400); + --accent-b-hover: var(--pink-300); + + --accent-c: var(--teal-400); + --accent-c-hover: var(--teal-300); + + /* Gradients (dark mode adjustments) */ + --radial-a-opacity-1: 0.16; + --radial-a-opacity-2: 0.48; + + /* Shadows (dark mode adjustments) */ + --shadow-color: hsla(var(--white), 0.04); + --shadow-svg-button-link-1: -2px 2px 12px 1px var(--shadow-color); + --shadow-svg-button-link-2: -6px 6px 12px -3px var(--shadow-color); + --shadow-svg-button-link-3: -12px 12px 24px -6px var(--shadow-color); + --shadow-svg-button-link-4: -20px 20px 40px -12px var(--shadow-color); + --shadow-svg-button-link-1-hover: 0px 0px 12px 2px var(--shadow-color); + --shadow-svg-button-link-2-hover: 0px 0px 12px 2px var(--shadow-color); + --shadow-svg-button-link-3-hover: 0px 0px 24px 2px var(--shadow-color); + --shadow-svg-button-link-4-hover: 0px 0px 40px 2px var(--shadow-color); + --shadow-body-color-a: hsla(var(--gray-600), 0.25); + --shadow-body-color-b: hsla(var(--gray-600), 0.05); + + --shadow-window-box-1-opacity: 0.1; + --shadow-window-box-2-opacity: 0.08; + --shadow-window-box-3-opacity: 0.16; + --shadow-window-box-4-opacity: 0.06; + --shadow-window-box-5-opacity: 0.06; + } +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 99341906484..9e0bb7b8948 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -25,6 +25,7 @@ const config = { monospace: "var(--font-mono)", }, fontSize: { + "7xl": ["4rem", "1.1"], // [7xl, 6xs] "6xl": ["3.75rem", "1.2"], // [6xl, 4xs] "5xl": ["3rem", "1.2"], // [5xl, 4xs] "4xl": ["2.25rem", "1.2"], // [4xl, 4xs] @@ -64,116 +65,165 @@ const config = { }, colors: { gray: { - 100: "var(--gray-100)", - 150: "var(--gray-150)", - 200: "var(--gray-200)", - 300: "var(--gray-300)", - 400: "var(--gray-400)", - 500: "var(--gray-500)", - 600: "var(--gray-600)", - 700: "var(--gray-700)", - 800: "var(--gray-800)", - 900: "var(--gray-900)", + 100: "hsla(var(--gray-100))", + 150: "hsla(var(--gray-150))", + 200: "hsla(var(--gray-200))", + 300: "hsla(var(--gray-300))", + 400: "hsla(var(--gray-400))", + 500: "hsla(var(--gray-500))", + 600: "hsla(var(--gray-600))", + 700: "hsla(var(--gray-700))", + 800: "hsla(var(--gray-800))", + 900: "hsla(var(--gray-900))", + }, + + purple: { + 50: "hsla(var(--purple-50))", + 100: "hsla(var(--purple-100))", + 200: "hsla(var(--purple-200))", + 300: "hsla(var(--purple-300))", + 400: "hsla(var(--purple-400))", + 500: "hsla(var(--purple-500))", + 600: "hsla(var(--purple-600))", + 700: "hsla(var(--purple-700))", + 800: "hsla(var(--purple-800))", + 900: "hsla(var(--purple-900))", }, blue: { - 50: "var(--blue-50)", - 100: "var(--blue-100)", - 200: "var(--blue-200)", - 300: "var(--blue-300)", - 400: "var(--blue-400)", - 500: "var(--blue-500)", - 600: "var(--blue-600)", - 700: "var(--blue-700)", - 800: "var(--blue-800)", - 900: "var(--blue-900)", + 50: "hsla(var(--blue-50))", + 100: "hsla(var(--blue-100))", + 200: "hsla(var(--blue-200))", + 300: "hsla(var(--blue-300))", + 400: "hsla(var(--blue-400))", + 500: "hsla(var(--blue-500))", + 600: "hsla(var(--blue-600))", + 700: "hsla(var(--blue-700))", + 800: "hsla(var(--blue-800))", + 900: "hsla(var(--blue-900))", }, orange: { - 50: "var(--orange-50)", - 100: "var(--orange-100)", - 200: "var(--orange-200)", - 300: "var(--orange-300)", - 400: "var(--orange-400)", - 500: "var(--orange-500)", - 550: "var(--orange-550)", - 600: "var(--orange-600)", - 700: "var(--orange-700)", - 800: "var(--orange-800)", - 900: "var(--orange-900)", + 50: "hsla(var(--orange-50))", + 100: "hsla(var(--orange-100))", + 200: "hsla(var(--orange-200))", + 300: "hsla(var(--orange-300))", + 400: "hsla(var(--orange-400))", + 500: "hsla(var(--orange-500))", + 550: "hsla(var(--orange-550))", + 600: "hsla(var(--orange-600))", + 700: "hsla(var(--orange-700))", + 800: "hsla(var(--orange-800))", + 900: "hsla(var(--orange-900))", }, primary: { - DEFAULT: "var(--primary)", - "high-contrast": "var(--primary-high-contrast)", - "low-contrast": "var(--primary-low-contrast)", - hover: "var(--primary-hover)", - visited: "var(--primary-visited)", - light: "var(--primary-light)", - dark: "var(--primary-dark)", - pressed: "var(--primary-pressed)", + DEFAULT: "hsla(var(--primary))", + "high-contrast": "hsla(var(--primary-high-contrast))", + "low-contrast": "hsla(var(--primary-low-contrast))", + hover: "hsla(var(--primary-hover))", + visited: "hsla(var(--primary-visited))", + action: "hsla(var(--primary-action))", + "action-hover": "hsla(var(--primary-action-hover))", + light: "hsla(var(--primary-light))" /* TODO: Migrate/deprecate */, + dark: "hsla(var(--primary-dark))" /* TODO: Migrate/deprecate */, + }, + accent: { + a: { + DEFAULT: "hsla(var(--accent-a))", + hover: "hsla(var(--accent-a-hover))", + }, + b: { + DEFAULT: "hsla(var(--accent-b))", + hover: "hsla(var(--accent-b-hover))", + }, + c: { + DEFAULT: "hsla(var(--accent-c))", + hover: "hsla(var(--accent-c-hover))", + }, }, body: { - DEFAULT: "var(--body)", - medium: "var(--body-medium)", - light: "var(--body-light)", - inverted: "var(--body-inverted)", + DEFAULT: "hsla(var(--body))", + medium: "hsla(var(--body-medium))", + light: "hsla(var(--body-light))", }, background: { - DEFAULT: "var(--background)", - highlight: "var(--background-highlight)", + DEFAULT: "hsla(var(--background))", + highlight: "hsla(var(--background-highlight))", }, - disabled: "var(--disabled)", - neutral: "var(--neutral)", + /** @deprecated */ + neutral: "hsla(var(--neutral))", // TODO: Migrate + /** @deprecated */ + "switch-background": "hsla(var(--switch-background))", // TODO: Migrate + disabled: "hsla(var(--disabled))", "tooltip-shadow": "var(--tooltip-shadow)", - "switch-background": "var(--switch-background)", "hub-hero-content-bg": "var(--hub-hero-content-bg)", "search-background": "var(--search-background)", attention: { - DEFAULT: "var(--attention)", - light: "var(--attention-light)", - outline: "var(--attention-outline)", + DEFAULT: "hsla(var(--attention))", + light: "hsla(var(--attention-light))", + outline: "hsla(var(--attention-outline))", }, error: { - DEFAULT: "var(--error)", - light: "var(--error-light)", - outline: "var(--error-outline)", - neutral: "var(--error-neutral)", + DEFAULT: "hsla(var(--error))", + light: "hsla(var(--error-light))", + outline: "hsla(var(--error-outline))", + neutral: "hsla(var(--error-neutral))", }, success: { - DEFAULT: "var(--success)", - light: "var(--success-light)", - outline: "var(--success-outline)", - neutral: "var(--success-neutral)", + DEFAULT: "hsla(var(--success))", + light: "hsla(var(--success-light))", + outline: "hsla(var(--success-outline))", + neutral: "hsla(var(--success-neutral))", }, }, backgroundImage: { - "main-gradient": "var(--main-gradient)", + "gradient-main": "var(--gradient-main)", + "gradient-banner": "var(--gradient-banner)", + "main-gradient": "var(--gradient-main)", // TODO: Duplicate; remove one "feedback-gradient": "var(--feedback-gradient)", "banner-grid-gradient": "var(--banner-grid-gradient)", + "radial-a": "var(--radial-a)", }, boxShadow: { "table-box": "var(--table-box-shadow)", - table: - "0 14px 66px rgba(0,0,0,.07), 0 10px 17px rgba(0,0,0,.03), 0 4px 7px rgba(0,0,0,.05)", + table: ` + 0 14px 66px rgba(0,0,0,.07), + 0 10px 17px rgba(0,0,0,.03), + 0 4px 7px rgba(0,0,0,.05)`, drop: "0 4px 17px 0 rgba(0,0,0,0.08)", "table-box-hover": "0px 8px 17px rgba(0, 0, 0, 0.15)", "table-item-box": "var(--table-item-box-shadow)", - "table-item-box-hover": "0 0 1px var(--primary)", + "table-item-box-hover": "0 0 1px hsla(var(--primary))", "grid-yellow-box-shadow": "8px 8px 0px 0px #ffe78e", "grid-blue-box-shadow": "8px 8px 0px 0px #a7d0f4", // Part of new DS - "menu-accordion": - "0px 2px 2px 0px rgba(0, 0, 0, 0.12) inset, 0px -3px 2px 0px rgba(0, 0, 0, 0.14) inset", + "menu-accordion": ` + 0px 2px 2px 0px rgba(0, 0, 0, 0.12) inset, + 0px -3px 2px 0px rgba(0, 0, 0, 0.14) inset`, // TODO: From current theme. Deprecate for 'button-hover' - primary: "4px 4px 0px 0px var(--primary)", - "button-hover": "4px 4px 0 0 var(--primary-low-contrast)", + primary: "4px 4px 0px 0px hsla(var(--primary))", + "button-hover": "4px 4px 0 0 hsla(var(--primary-low-contrast))", tooltip: "0 0 16px var(--tooltip-shadow)", + "svg-button-link": ` + var(--shadow-svg-button-link-1), var(--shadow-svg-button-link-2), + var(--shadow-svg-button-link-3), var(--shadow-svg-button-link-4)`, + "svg-button-link-hover": ` + var(--shadow-svg-button-link-1-hover), + var(--shadow-svg-button-link-2-hover), + var(--shadow-svg-button-link-3-hover), + var(--shadow-svg-button-link-4-hover)`, + "card-hover": "var(--shadow-body-md), var(--shadow-body-lg)", + "window-box": ` + var(--shadow-window-box-1), var(--shadow-window-box-2), + var(--shadow-window-box-3), var(--shadow-window-box-4), + var(--shadow-window-box-5)`, }, spacing: { 7.5: "1.875rem", 10.5: "2.625rem", 19: "4.75rem", // Nav height 31: "7.75rem", // FeedbackWidget conditional bottom offset + 128: "32rem", }, keyframes: { "accordion-down": { @@ -189,6 +239,13 @@ const config = { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, + // Add custom border-radius tailwinds extension for "4xl" as "2rem" + borderRadius: { + "4xl": "2rem" /* 32px */, + }, + gridTemplateColumns: { + bento: "2rem repeat(10, 1fr) 2rem", + }, textUnderlineOffset: { 3: "3px", }, diff --git a/yarn.lock b/yarn.lock index 4585f156f42..3b7c075e1a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6201,6 +6201,13 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded" integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg== +"@types/swiper@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/swiper/-/swiper-6.0.0.tgz#9934ecd569611b660a2a9bf200f25ce5ba4b4d63" + integrity sha512-QPZRgxZ+ivXXtzV43B3LxpXUIC7FE/EoKM+rtxngmgt2M7eeUYypZhyqZD8UxJtlBcUDw/ATGoVeSNpvBBrz2w== + dependencies: + swiper "*" + "@types/ungap__structured-clone@^0.3.0": version "0.3.3" resolved "https://registry.yarnpkg.com/@types/ungap__structured-clone/-/ungap__structured-clone-0.3.3.tgz#cf7e1252f18f5ee39291a8f52fa83c31b0102fc6" @@ -6221,6 +6228,13 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== +"@types/xml2js@^0.4.14": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a" + integrity sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^6.19.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" @@ -13073,7 +13087,7 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -"prettier-fallback@npm:prettier@^3": +"prettier-fallback@npm:prettier@^3", prettier@^3.1.1: version "3.3.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== @@ -13088,11 +13102,6 @@ prettier@^2.0.5, prettier@^2.8.8: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -prettier@^3.1.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" - integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== - prettier@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" @@ -14004,6 +14013,11 @@ sass-loader@^12.4.0: klona "^2.0.4" neo-async "^2.6.2" +sax@>=0.6.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -14702,6 +14716,16 @@ svgo@^3.0.2: csso "^5.0.5" picocolors "^1.0.0" +swiper@*: + version "11.1.9" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.1.9.tgz#55505c7cf4723b678df8220fc06152b793585dbc" + integrity sha512-rflu8zvfGa3x1v/aeSufk4zRJffhOQowyvtJlp46sUBnOqAuk1Rdv5Ldj0AWWBV595iZ+ZMk7VB35ZRtRUomtA== + +swiper@^11.1.10: + version "11.1.10" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.1.10.tgz#4d3df50ff8afc4960e9644ed6e5828d35ab38853" + integrity sha512-pAVM6vCb6bumj2B9aSh67l3wP1j5YR8dPQM1YhQKMpnBc33vs+RpyVz6NZYZl/ZopCBSYbbWK5nvESwbmU0QXQ== + tailwind-merge@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.4.0.tgz#1345209dc1f484f15159c9180610130587703042" @@ -15829,6 +15853,19 @@ ws@^8.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== +xml2js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"