mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 13:34:06 +01:00
feat: Add shadcn Carousel for image slideshow on Invest page
This commit is contained in:
parent
3ee06d8002
commit
f27c3a19b9
@ -17,6 +17,7 @@
|
||||
"@supabase/supabase-js": "^2.45.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
"lucide-react": "^0.428.0",
|
||||
"next": "14.2.5",
|
||||
"next-themes": "^0.3.0",
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@radix-ui/react-dropdown-menu':
|
||||
specifier: ^2.1.1
|
||||
@ -29,6 +25,9 @@ dependencies:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
embla-carousel-react:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0(react@18.3.1)
|
||||
lucide-react:
|
||||
specifier: ^0.428.0
|
||||
version: 0.428.0(react@18.3.1)
|
||||
@ -1502,6 +1501,28 @@ packages:
|
||||
/eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
/embla-carousel-react@8.2.0(react@18.3.1):
|
||||
resolution: {integrity: sha512-dWqbmaEBQjeAcy/EKrcAX37beVr0ubXuHPuLZkx27z58V1FIvRbbMb4/c3cLZx0PAv/ofngX2QFrwUB+62SPnw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.1 || ^18.0.0
|
||||
dependencies:
|
||||
embla-carousel: 8.2.0
|
||||
embla-carousel-reactive-utils: 8.2.0(embla-carousel@8.2.0)
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/embla-carousel-reactive-utils@8.2.0(embla-carousel@8.2.0):
|
||||
resolution: {integrity: sha512-ZdaPNgMydkPBiDRUv+wRIz3hpZJ3LKrTyz+XWi286qlwPyZFJDjbzPBiXnC3czF9N/nsabSc7LTRvGauUzwKEg==}
|
||||
peerDependencies:
|
||||
embla-carousel: 8.2.0
|
||||
dependencies:
|
||||
embla-carousel: 8.2.0
|
||||
dev: false
|
||||
|
||||
/embla-carousel@8.2.0:
|
||||
resolution: {integrity: sha512-rf2GIX8rab9E6ZZN0Uhz05746qu2KrDje9IfFyHzjwxLwhvGjUt6y9+uaY1Sf+B0OPSa3sgas7BE2hWZCtopTA==}
|
||||
dev: false
|
||||
|
||||
/emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@ -3656,3 +3677,7 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
35
src/app/invest/page.tsx
Normal file
35
src/app/invest/page.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@/components/ui/carousel"
|
||||
|
||||
export default function Invest() {
|
||||
return (
|
||||
<div className=" w-[90%] h-[500px] bg-red-500 ml-20 mt-12">
|
||||
<div>
|
||||
<div className="flex">
|
||||
<Image src="./logo.svg" alt="logo" width={50} height={50} />
|
||||
<h1 className="mt-3 font-bold text-3xl">NVIDIA</h1>
|
||||
</div>
|
||||
<p className="mt-2"> World's first non-metal sustainable battery</p>
|
||||
<div className="flex mt-3">
|
||||
{["Technology", "Gaming"].map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1 mx-1"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -78,10 +78,10 @@ export default function Home() {
|
||||
<p className="text-lg">The deals attracting the most interest right now</p>
|
||||
</span>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<BusinessCard description={null} joinDate={null} location={null} tags={null} />
|
||||
<BusinessCard description={null} joinDate={null} location={null} tags={null} />
|
||||
<BusinessCard description={null} joinDate={null} location={null} tags={null} />
|
||||
<BusinessCard description={null} joinDate={null} location={null} tags={null} />
|
||||
<BusinessCard name={"NVDA"} description={"Founded in 1993, NVIDIA is a key innovator of computer graphics and AI technology"} joinDate={"December 2021"} location={"Bangkok, Thailand"} tags={null} />
|
||||
<BusinessCard name={"Apple Inc."} description={"Founded in 1976, Apple Inc. is a leading innovator in consumer electronics, software, and online services, known for products like the iPhone, MacBook, and the App Store."} joinDate={"February 2020"} location={"Cupertino, California, USA"} tags={null} />
|
||||
<BusinessCard name={"Google LLC"} description={"Founded in 1998, Google LLC specializes in internet-related services and products, including search engines, online advertising, cloud computing, and the Android operating system."} joinDate={"April 2019"} location={"Mountain View, California, USA"} tags={null} />
|
||||
<BusinessCard name={"Microsoft Corporation"} description={"Founded in 1975, Microsoft Corporation is a multinational technology company that develops, manufactures, and licenses software, hardware, and services, including Windows, Office, and Azure."} joinDate={"January 2018"} location={""} tags={null} />
|
||||
</div>
|
||||
<div className="self-center py-5">
|
||||
<Button>
|
||||
|
||||
@ -9,13 +9,15 @@ import {
|
||||
import { CalendarDaysIcon } from "lucide-react";
|
||||
|
||||
interface XMap {
|
||||
// tagName: colorCode
|
||||
[tag: string]: string;
|
||||
}
|
||||
|
||||
interface BusinessCardProps {
|
||||
description: string | null;
|
||||
joinDate: string | null;
|
||||
location: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
joinDate: string;
|
||||
location: string;
|
||||
tags: XMap | null;
|
||||
}
|
||||
|
||||
@ -23,7 +25,7 @@ export function BusinessCard(props: BusinessCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="h-[200px] pb-2">
|
||||
<div className="h-[200px] hover:h-[100px] duration-75 pb-2">
|
||||
<Image
|
||||
src={"/money.png"}
|
||||
width={0}
|
||||
@ -33,18 +35,17 @@ export function BusinessCard(props: BusinessCardProps) {
|
||||
alt="nvidia"
|
||||
/>
|
||||
</div>
|
||||
<CardTitle>NVIDIA</CardTitle>
|
||||
<CardTitle>{props.name}</CardTitle>
|
||||
<CardDescription>
|
||||
Founded in 1993, NVIDIA is a key innovator of computer graphics and AI
|
||||
technology
|
||||
{props.description}
|
||||
<span className="flex items-center pt-2 gap-1">
|
||||
<CalendarDaysIcon width={20} />
|
||||
Joined December 2021
|
||||
Joined {props.joinDate}
|
||||
</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start">
|
||||
Bangkok, Thailand
|
||||
{props.location}
|
||||
<span className="text-xs rounded-md bg-slate-200 dark:bg-slate-700 p-1">
|
||||
Technology
|
||||
</span>
|
||||
|
||||
262
src/components/ui/carousel.tsx
Normal file
262
src/components/ui/carousel.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
29
src/components/ui/hover-card.tsx
Normal file
29
src/components/ui/hover-card.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
Loading…
Reference in New Issue
Block a user