From 74ec86c684fd27f44793ec0de5c635a61c4b6a2a Mon Sep 17 00:00:00 2001 From: Mathis Date: Thu, 6 Jun 2024 14:03:45 +0200 Subject: [PATCH] feat: Add multiple UI components to the codebase This commit includes several User Interface components such as Button, DropdownMenu, FlipWords, HeroParallax, Input, Label, NavigationMenu and ToastBox. Each component was developed individually and has different attributes and behaviors. This will significantly improve the user experience and interactivity of the application. --- src/components/ui/button.tsx | 56 ++++++++ src/components/ui/dropdown-menu.tsx | 200 ++++++++++++++++++++++++++ src/components/ui/flip-words.tsx | 84 +++++++++++ src/components/ui/hero-parallax.tsx | 147 +++++++++++++++++++ src/components/ui/input.tsx | 25 ++++ src/components/ui/label.tsx | 26 ++++ src/components/ui/navigation-menu.tsx | 128 +++++++++++++++++ src/components/ui/toast-box.tsx | 63 ++++++++ 8 files changed, 729 insertions(+) create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/flip-words.tsx create mode 100644 src/components/ui/hero-parallax.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/navigation-menu.tsx create mode 100644 src/components/ui/toast-box.tsx diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0ba4277 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/components/ui/flip-words.tsx b/src/components/ui/flip-words.tsx new file mode 100644 index 0000000..155d41c --- /dev/null +++ b/src/components/ui/flip-words.tsx @@ -0,0 +1,84 @@ +"use client"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion, LayoutGroup } from "framer-motion"; +import { cn } from "@/lib/utils"; + +export const FlipWords = ({ + words, + duration = 3000, + className, + }: { + words: string[]; + duration?: number; + className?: string; +}) => { + const [currentWord, setCurrentWord] = useState(words[0]); + const [isAnimating, setIsAnimating] = useState(false); + + const startAnimation = useCallback(() => { + const word = words[words.indexOf(currentWord) + 1] || words[0]; + setCurrentWord(word); + setIsAnimating(true); + }, [currentWord, words]); + + useEffect(() => { + if (!isAnimating) + setTimeout(() => { + startAnimation(); + }, duration); + }, [isAnimating, duration, startAnimation]); + + return ( + { + setIsAnimating(false); + }} + > + + {currentWord.split("").map((letter, index) => ( + + {letter} + + ))} + + + ); +}; diff --git a/src/components/ui/hero-parallax.tsx b/src/components/ui/hero-parallax.tsx new file mode 100644 index 0000000..e5caf16 --- /dev/null +++ b/src/components/ui/hero-parallax.tsx @@ -0,0 +1,147 @@ +"use client"; +import React from "react"; +import { + motion, + useScroll, + useTransform, + useSpring, + MotionValue, +} from "framer-motion"; +import Image from "next/image"; +import Link from "next/link"; + +interface HeroParallaxProps { + products: product[]; + children?: React.ReactNode +} + +interface product { + title: string; + link: string; + thumbnail: string; +} + +export const HeroParallax = ({ products, children }: HeroParallaxProps ) => { + const firstRow = products.slice(0, 5); + const secondRow = products.slice(5, 10); + const thirdRow = products.slice(10, 15); + const ref = React.useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ["start start", "end start"], + }); + + const springConfig = { stiffness: 300, damping: 30, bounce: 100 }; + + const translateX = useSpring( + useTransform(scrollYProgress, [0, 1], [0, 1000]), + springConfig + ); + const translateXReverse = useSpring( + useTransform(scrollYProgress, [0, 1], [0, -1000]), + springConfig + ); + const rotateX = useSpring( + useTransform(scrollYProgress, [0, 0.2], [15, 0]), + springConfig + ); + const opacity = useSpring( + useTransform(scrollYProgress, [0, 0.2], [0.2, 1]), + springConfig + ); + const rotateZ = useSpring( + useTransform(scrollYProgress, [0, 0.2], [20, 0]), + springConfig + ); + const translateY = useSpring( + useTransform(scrollYProgress, [0, 0.2], [-700, 500]), + springConfig + ); + return ( +
+ {children} + + + {firstRow.map((product) => ( + + ))} + + + {secondRow.map((product) => ( + + ))} + + + {thirdRow.map((product) => ( + + ))} + + +
+ ); +}; + +export const ProductCard = ({ + product, + translate, + }: { + product: { + title: string; + link: string; + thumbnail: string; + }; + translate: MotionValue; +}) => { + return ( + + + {product.title} + +
+

+ {product.title} +

+
+ ); +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..677d05f --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..8af8f00 --- /dev/null +++ b/src/components/ui/navigation-menu.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" +import { cva } from "class-variance-authority" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)) +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName + +const NavigationMenuItem = NavigationMenuPrimitive.Item + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" +) + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children}{" "} + +)) +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName + +const NavigationMenuLink = NavigationMenuPrimitive.Link + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ +
+)) +NavigationMenuViewport.displayName = + NavigationMenuPrimitive.Viewport.displayName + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
+ +)) +NavigationMenuIndicator.displayName = + NavigationMenuPrimitive.Indicator.displayName + +export { + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, +} diff --git a/src/components/ui/toast-box.tsx b/src/components/ui/toast-box.tsx new file mode 100644 index 0000000..d2842f3 --- /dev/null +++ b/src/components/ui/toast-box.tsx @@ -0,0 +1,63 @@ +import {Bug, CircleAlert, CircleCheckBig, CircleHelp, MessageSquareText, Minus, OctagonX, Plus} from "lucide-react"; +import React from "react"; + + +export enum toastType { + info = "info", + warn = "warn", + error = "error", + refused= "refused", + success = "success", + add = "add", + del = "del", + message = "message" +} + +export function ToastBox({title, message, type}: {title?: string, message: string, type: toastType}) { + let icon: any; + let bgColor: string; + switch (type) { + case toastType.message : + icon = + bgColor = 'bg-accent' + break + case toastType.add: + icon = + bgColor = 'bg-accent' + break + case toastType.del: + icon = + bgColor = 'bg-accent' + break + case toastType.info: + icon = + bgColor = 'bg-accent' + break + case toastType.warn: + icon = + bgColor = 'bg-orange-500' + break + case toastType.error: + icon = + bgColor = 'bg-red-500' + break + case toastType.success: + icon = + bgColor = 'bg-green-500' + break + case toastType.refused: + icon = + bgColor = 'bg-red-700' + break + } + + return ( +
+ {icon} +
+ {title &&

{title}

} +

{message}

+
+
+ ) +} \ No newline at end of file