From 5f6c5f08b735c71a815c2165cebb89223cad4016 Mon Sep 17 00:00:00 2001 From: "dnncrye.dev" <29934021+mtlaso@users.noreply.github.com> Date: Sun, 19 Jan 2025 00:22:33 -0500 Subject: [PATCH 1/2] animations --- messages/fr.json | 6 +- package.json | 2 + pnpm-lock.yaml | 72 ++++ src/app/[locale]/(landing)/page.tsx | 148 +++++--- src/app/[locale]/ui/settings/settings.tsx | 1 - src/app/[locale]/ui/spacing.ts | 1 + src/components/ui/shimmer-button.tsx | 97 ++++++ src/components/ui/text-animate.tsx | 391 ++++++++++++++++++++++ tailwind.config.ts | 129 ++++--- 9 files changed, 740 insertions(+), 107 deletions(-) create mode 100644 src/components/ui/shimmer-button.tsx create mode 100644 src/components/ui/text-animate.tsx diff --git a/messages/fr.json b/messages/fr.json index de58b41..1bf9a14 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -11,12 +11,12 @@ { "key": "0", "title": "Sauvegardez du contenu à lire", - "description": "Sauvegardez n'importe quel lien pour le lire plus tard. Triez, archivez et organisez comme bon vous semble." + "description": "Sauvegardez n'importe quel lien pour le lire plus tard." }, { "key": "1", "title": "Suivez vos sources préférées", - "description": "Blogs, YouTube, Reddit... Restez à jour avec tous vos contenus préférés grâce aux flux RSS et Atom." + "description": "Blogs, YouTube, Reddit... Restez à jour avec tous vos contenus grâce aux flux RSS et Atom." }, { "key": "2", @@ -204,7 +204,7 @@ }, "exportDataSection": { "title": "Exporter données du compte", - "description": "Exporter vos données au format JSON.", + "description": "Exporter vos données.", "export": "Exporter" }, "deleteAccountSection": { diff --git a/package.json b/package.json index 17d7ff2..111f030 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-radio-group": "^1.2.2", @@ -36,6 +37,7 @@ "isomorphic-dompurify": "^2.19.0", "jsdom": "^25.0.1", "lucide-react": "^0.460.0", + "motion": "^11.18.1", "next": "^15.1.3", "next-auth": "5.0.0-beta.25", "next-intl": "^3.26.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32e7464..d77903c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.4 version: 2.1.4(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -93,6 +96,9 @@ importers: lucide-react: specifier: ^0.460.0 version: 0.460.0(react@19.0.0) + motion: + specifier: ^11.18.1 + version: 11.18.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^15.1.3 version: 15.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -935,6 +941,11 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-icons@1.3.2': + resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -1697,6 +1708,20 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + framer-motion@11.18.1: + resolution: {integrity: sha512-EQa8c9lWVOm4zlz14MsBJWr8woq87HsNmsBnQNvcS0hs8uzw6HtGAxZyIU7EGTVpHD1C1n01ufxRyarXcNzpPg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2010,6 +2035,26 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} + + motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} + + motion@11.18.1: + resolution: {integrity: sha512-Tqbn7UMVp1V6JH5gbLFJAbP1//gdHNwEciQO22UHgFH1mBScq6gHrq8dlyBGbOlRF8iVSX8k0UV8tkkAVbwpKw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3327,6 +3372,10 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@radix-ui/react-icons@1.3.2(react@19.0.0)': + dependencies: + react: 19.0.0 + '@radix-ui/react-id@1.1.0(@types/react@19.0.1)(react@19.0.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) @@ -4086,6 +4135,15 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + framer-motion@11.18.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + motion-dom: 11.18.1 + motion-utils: 11.18.1 + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + fsevents@2.3.3: optional: true @@ -4639,6 +4697,20 @@ snapshots: mitt@3.0.1: {} + motion-dom@11.18.1: + dependencies: + motion-utils: 11.18.1 + + motion-utils@11.18.1: {} + + motion@11.18.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + framer-motion: 11.18.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + ms@2.1.3: {} mz@2.7.0: diff --git a/src/app/[locale]/(landing)/page.tsx b/src/app/[locale]/(landing)/page.tsx index 89f5a7f..f1be5ca 100644 --- a/src/app/[locale]/(landing)/page.tsx +++ b/src/app/[locale]/(landing)/page.tsx @@ -1,5 +1,11 @@ import { SPACING } from "@/app/[locale]/ui/spacing"; import { Button } from "@/components/ui/button"; +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { NavigationMenu, NavigationMenuItem, @@ -7,96 +13,130 @@ import { NavigationMenuList, navigationMenuTriggerStyle, } from "@/components/ui/navigation-menu"; +import { ShimmerButton } from "@/components/ui/shimmer-button"; +import { TextAnimate } from "@/components/ui/text-animate"; import { Link } from "@/i18n/routing"; import { cn } from "@/lib/utils"; import { useTranslations } from "next-intl"; +import { TbArchive, TbMail, TbRss } from "react-icons/tb"; export default function Home(): React.JSX.Element { const t = useTranslations("metadata"); return ( <> + + + + + {t("title")} + + + {t("login")} + + + + +
- - - - - {t("title")} - - - {t("login")} - - - - - {/* hero */} -
-

+
+ {t("headline")} -

+ -

{t("description")}

- - - + + {t("description")} + + +
+ + + {t("ctaButton")} + + +
{/* features */}
{t.raw("features")?.map( (feature: { - key: number; + key: string; title: string; description: string; }) => ( -
-

- {feature.title} -

-

{feature.description}

-
+ + + +
+ {feature.key === "0" && } + {feature.key === "1" && } + {feature.key === "2" && } +
+
{feature.title}
+
+ {feature.description} +
+
), )}
- {/* last cta */} -
+ {/* cta */} +

{t("lastCtaHeadline")}

+

{t("lastCtaDescription")}

- - - +
+ + + +
diff --git a/src/app/[locale]/ui/settings/settings.tsx b/src/app/[locale]/ui/settings/settings.tsx index 469b608..e84198d 100644 --- a/src/app/[locale]/ui/settings/settings.tsx +++ b/src/app/[locale]/ui/settings/settings.tsx @@ -19,7 +19,6 @@ export function Settings({ name, feedContentLimit, }: Props): React.JSX.Element { - const _t = useTranslations("settings"); return (
diff --git a/src/app/[locale]/ui/spacing.ts b/src/app/[locale]/ui/spacing.ts index d0bb058..78af7cb 100644 --- a/src/app/[locale]/ui/spacing.ts +++ b/src/app/[locale]/ui/spacing.ts @@ -2,4 +2,5 @@ export const SPACING = { SM: "space-y-2", // SECTIONS_SPACING: "mb-12", MD: "space-y-6", + LG: "space-y-12", } as const; diff --git a/src/components/ui/shimmer-button.tsx b/src/components/ui/shimmer-button.tsx new file mode 100644 index 0000000..5965e6d --- /dev/null +++ b/src/components/ui/shimmer-button.tsx @@ -0,0 +1,97 @@ +import React, { type CSSProperties } from "react"; + +import { cn } from "@/lib/utils"; + +export interface ShimmerButtonProps + extends React.ButtonHTMLAttributes { + shimmerColor?: string; + shimmerSize?: string; + borderRadius?: string; + shimmerDuration?: string; + background?: string; + className?: string; + children?: React.ReactNode; +} + +export const ShimmerButton = React.forwardRef< + HTMLButtonElement, + ShimmerButtonProps +>( + ( + { + shimmerColor = "#ffffff", + shimmerSize = "0.05em", + shimmerDuration = "3s", + borderRadius = "100px", + background = "rgba(0, 0, 0, 1)", + className, + children, + ...props + }, + ref, + ): React.JSX.Element => { + return ( + + ); + }, +); + +ShimmerButton.displayName = "ShimmerButton"; diff --git a/src/components/ui/text-animate.tsx b/src/components/ui/text-animate.tsx new file mode 100644 index 0000000..60d17dc --- /dev/null +++ b/src/components/ui/text-animate.tsx @@ -0,0 +1,391 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { + AnimatePresence, + type MotionProps, + type Variants, + motion, +} from "motion/react"; +import type { ElementType } from "react"; + +type AnimationType = "text" | "word" | "character" | "line"; +type AnimationVariant = + | "fadeIn" + | "blurIn" + | "blurInUp" + | "blurInDown" + | "slideUp" + | "slideDown" + | "slideLeft" + | "slideRight" + | "scaleUp" + | "scaleDown"; + +interface TextAnimateProps extends MotionProps { + /** + * The text content to animate + */ + children: string; + /** + * The class name to be applied to the component + */ + className?: string; + /** + * The class name to be applied to each segment + */ + segmentClassName?: string; + /** + * The delay before the animation starts + */ + delay?: number; + /** + * The duration of the animation + */ + duration?: number; + /** + * Custom motion variants for the animation + */ + variants?: Variants; + /** + * The element type to render + */ + as?: ElementType; + /** + * How to split the text ("text", "word", "character") + */ + by?: AnimationType; + /** + * Whether to start animation when component enters viewport + */ + startOnView?: boolean; + /** + * Whether to animate only once + */ + once?: boolean; + /** + * The animation preset to use + */ + animation?: AnimationVariant; +} + +const staggerTimings: Record = { + text: 0.06, + word: 0.05, + character: 0.03, + line: 0.06, +}; + +const defaultContainerVariants = { + hidden: { opacity: 1 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.05, + }, + }, + exit: { + opacity: 0, + transition: { + staggerChildren: 0.05, + staggerDirection: -1, + }, + }, +}; + +const defaultItemVariants: Variants = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + }, + exit: { + opacity: 0, + }, +}; + +const defaultItemAnimationVariants: Record< + AnimationVariant, + { container: Variants; item: Variants } +> = { + fadeIn: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, y: 20 }, + show: (delay: number) => ({ + opacity: 1, + y: 0, + transition: { + delay, + duration: 0.3, + }, + }), + exit: { + opacity: 0, + y: 20, + transition: { duration: 0.3 }, + }, + }, + }, + blurIn: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, filter: "blur(10px)" }, + show: (i: number) => ({ + opacity: 1, + filter: "blur(0px)", + transition: { + delay: i * 0.1, + duration: 0.3, + }, + }), + exit: { + opacity: 0, + filter: "blur(10px)", + transition: { duration: 0.3 }, + }, + }, + }, + blurInUp: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, filter: "blur(10px)", y: 20 }, + show: (_delay: number) => ({ + opacity: 1, + filter: "blur(0px)", + y: 0, + transition: { + y: { duration: 0.3 }, + opacity: { duration: 0.4 }, + filter: { duration: 0.3 }, + }, + }), + exit: { + opacity: 0, + filter: "blur(10px)", + y: 20, + transition: { + y: { duration: 0.3 }, + opacity: { duration: 0.4 }, + filter: { duration: 0.3 }, + }, + }, + }, + }, + blurInDown: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, filter: "blur(10px)", y: -20 }, + show: (_delay: number) => ({ + opacity: 1, + filter: "blur(0px)", + y: 0, + transition: { + y: { duration: 0.3 }, + opacity: { duration: 0.4 }, + filter: { duration: 0.3 }, + }, + }), + }, + }, + slideUp: { + container: defaultContainerVariants, + item: { + hidden: { y: 20, opacity: 0 }, + show: (delay: number) => ({ + y: 0, + opacity: 1, + transition: { + delay, + duration: 0.3, + }, + }), + exit: { + y: -20, + opacity: 0, + transition: { + duration: 0.3, + }, + }, + }, + }, + slideDown: { + container: defaultContainerVariants, + item: { + hidden: { y: -20, opacity: 0 }, + show: { + y: 0, + opacity: 1, + transition: { duration: 0.3 }, + }, + exit: { + y: 20, + opacity: 0, + transition: { duration: 0.3 }, + }, + }, + }, + slideLeft: { + container: defaultContainerVariants, + item: { + hidden: { x: 20, opacity: 0 }, + show: { + x: 0, + opacity: 1, + transition: { duration: 0.3 }, + }, + exit: { + x: -20, + opacity: 0, + transition: { duration: 0.3 }, + }, + }, + }, + slideRight: { + container: defaultContainerVariants, + item: { + hidden: { x: -20, opacity: 0 }, + show: { + x: 0, + opacity: 1, + transition: { duration: 0.3 }, + }, + exit: { + x: 20, + opacity: 0, + transition: { duration: 0.3 }, + }, + }, + }, + scaleUp: { + container: defaultContainerVariants, + item: { + hidden: { scale: 0.5, opacity: 0 }, + show: { + scale: 1, + opacity: 1, + transition: { + duration: 0.3, + scale: { + type: "spring", + damping: 15, + stiffness: 300, + }, + }, + }, + exit: { + scale: 0.5, + opacity: 0, + transition: { duration: 0.3 }, + }, + }, + }, + scaleDown: { + container: defaultContainerVariants, + item: { + hidden: { scale: 1.5, opacity: 0 }, + show: (delay: number) => ({ + scale: 1, + opacity: 1, + transition: { + delay, + duration: 0.3, + scale: { + type: "spring", + damping: 15, + stiffness: 300, + }, + }, + }), + exit: { + scale: 1.5, + opacity: 0, + transition: { duration: 0.3 }, + }, + }, + }, +}; + +export function TextAnimate({ + children, + delay = 0, + duration = 0.3, + variants, + className, + segmentClassName, + as: Component = "p", + startOnView = true, + once = false, + by = "word", + animation = "fadeIn", + ...props +}: TextAnimateProps): React.JSX.Element { + const MotionComponent = motion.create(Component); + + // Use provided variants or default variants based on animation type + const finalVariants = animation + ? { + container: { + ...defaultItemAnimationVariants[animation].container, + show: { + ...defaultItemAnimationVariants[animation].container.show, + transition: { + staggerChildren: staggerTimings[by], + }, + }, + exit: { + ...defaultItemAnimationVariants[animation].container.exit, + transition: { + staggerChildren: staggerTimings[by], + staggerDirection: -1, + }, + }, + }, + item: defaultItemAnimationVariants[animation].item, + } + : { container: defaultContainerVariants, item: defaultItemVariants }; + + let segments: string[] = []; + switch (by) { + case "word": + segments = children.split(/(\s+)/); + break; + case "character": + segments = children.split(""); + break; + case "line": + segments = children.split("\n"); + break; + default: + segments = [children]; + break; + } + + return ( + + + {segments.map((segment, i) => ( + + {segment} + + ))} + + + ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 1c1ce1e..6d383ee 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -8,55 +8,86 @@ export default { "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - extend: { - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - } - } + extend: { + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + animation: { + 'shimmer-slide': 'shimmer-slide var(--speed) ease-in-out infinite alternate', + 'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear', + 'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear' + }, + keyframes: { + 'shimmer-slide': { + to: { + transform: 'translate(calc(100cqw - 100%), 0)' + } + }, + 'spin-around': { + '0%': { + transform: 'translateZ(0) rotate(0)' + }, + '15%, 35%': { + transform: 'translateZ(0) rotate(90deg)' + }, + '65%, 85%': { + transform: 'translateZ(0) rotate(270deg)' + }, + '100%': { + transform: 'translateZ(0) rotate(360deg)' + } + }, + 'border-beam': { + '100%': { + 'offset-distance': '100%' + } + } + } + } }, plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], } satisfies Config; From eae4a93344bbc5e45d0a51c3eb22e21705f7ca41 Mon Sep 17 00:00:00 2001 From: "dnncrye.dev" <29934021+mtlaso@users.noreply.github.com> Date: Sun, 19 Jan 2025 00:39:37 -0500 Subject: [PATCH 2/2] feat: refonte landing page --- src/app/[locale]/(landing)/page.tsx | 203 ++++++++++++++++------------ src/components/ui/blur-fade.tsx | 78 +++++++++++ 2 files changed, 192 insertions(+), 89 deletions(-) create mode 100644 src/components/ui/blur-fade.tsx diff --git a/src/app/[locale]/(landing)/page.tsx b/src/app/[locale]/(landing)/page.tsx index f1be5ca..2b05e3e 100644 --- a/src/app/[locale]/(landing)/page.tsx +++ b/src/app/[locale]/(landing)/page.tsx @@ -1,4 +1,5 @@ import { SPACING } from "@/app/[locale]/ui/spacing"; +import BlurFade from "@/components/ui/blur-fade"; import { Button } from "@/components/ui/button"; import { Card, @@ -14,7 +15,6 @@ import { navigationMenuTriggerStyle, } from "@/components/ui/navigation-menu"; import { ShimmerButton } from "@/components/ui/shimmer-button"; -import { TextAnimate } from "@/components/ui/text-animate"; import { Link } from "@/i18n/routing"; import { cn } from "@/lib/utils"; import { useTranslations } from "next-intl"; @@ -22,6 +22,23 @@ import { TbArchive, TbMail, TbRss } from "react-icons/tb"; export default function Home(): React.JSX.Element { const t = useTranslations("metadata"); + const elements: { + el: () => React.JSX.Element; + index: number; + }[] = [ + { + el: HeroSection, + index: 0, + }, + { + el: FeaturesSection, + index: 1, + }, + { + el: CtaSection, + index: 2, + }, + ]; return ( <> @@ -50,94 +67,11 @@ export default function Home(): React.JSX.Element { SPACING.LG, )} > - {/* hero */} -
- - {t("headline")} - - - - {t("description")} - - -
- - - {t("ctaButton")} - - -
-
- - {/* features */} -
- {t.raw("features")?.map( - (feature: { - key: string; - title: string; - description: string; - }) => ( - - - -
- {feature.key === "0" && } - {feature.key === "1" && } - {feature.key === "2" && } -
-
{feature.title}
-
- {feature.description} -
-
- ), - )} -
- - {/* cta */} -
-

- {t("lastCtaHeadline")} -

- -

{t("lastCtaDescription")}

-
- - - -
-
+ {elements.map((el) => ( + + {el.el()} + + ))}