diff --git a/bun.lock b/bun.lock index 5bcd5c3..aff94da 100644 --- a/bun.lock +++ b/bun.lock @@ -14,9 +14,10 @@ "@vercel/speed-insights": "^1.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.16", "lucide-react": "^0.542.0", "motion": "^12.23.12", - "next": "15.5.2", + "next": "^15.5.3", "next-themes": "^0.4.6", "ogl": "^1.0.11", "react": "19.1.0", @@ -120,23 +121,23 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], - "@next/env": ["@next/env@15.5.2", "", {}, "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg=="], + "@next/env": ["@next/env@15.5.3", "", {}, "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -272,7 +273,7 @@ "fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="], - "framer-motion": ["framer-motion@12.23.12", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg=="], + "framer-motion": ["framer-motion@12.23.16", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N81A8hiHqVsexOzI3wzkibyLURW1nEJsZaRuctPhG4AdbbciYu+bKJq9I2lQFzAO4Bx3h4swI6pBbF/Hu7f7BA=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -330,7 +331,7 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "next": ["next@15.5.2", "", { "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.2", "@next/swc-darwin-x64": "15.5.2", "@next/swc-linux-arm64-gnu": "15.5.2", "@next/swc-linux-arm64-musl": "15.5.2", "@next/swc-linux-x64-gnu": "15.5.2", "@next/swc-linux-x64-musl": "15.5.2", "@next/swc-win32-arm64-msvc": "15.5.2", "@next/swc-win32-x64-msvc": "15.5.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q=="], + "next": ["next@15.5.3", "", { "dependencies": { "@next/env": "15.5.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.3", "@next/swc-darwin-x64": "15.5.3", "@next/swc-linux-arm64-gnu": "15.5.3", "@next/swc-linux-arm64-musl": "15.5.3", "@next/swc-linux-x64-gnu": "15.5.3", "@next/swc-linux-x64-musl": "15.5.3", "@next/swc-win32-arm64-msvc": "15.5.3", "@next/swc-win32-x64-msvc": "15.5.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -410,6 +411,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "motion/framer-motion": ["framer-motion@12.23.12", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], } } diff --git a/next.config.ts b/next.config.ts index dac4013..e45cfb0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + ], + }, async redirects() { return [ { diff --git a/package.json b/package.json index be18a3c..61820e3 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,10 @@ "@vercel/speed-insights": "^1.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.16", "lucide-react": "^0.542.0", "motion": "^12.23.12", - "next": "15.5.2", + "next": "^15.5.3", "next-themes": "^0.4.6", "ogl": "^1.0.11", "react": "19.1.0", diff --git a/src/actions/github.ts b/src/actions/github.ts index 2514b26..ae12362 100644 --- a/src/actions/github.ts +++ b/src/actions/github.ts @@ -17,6 +17,32 @@ export async function getRepoStars(owner: string, repo: string) { return data.stargazers_count as number; } +export async function getRepoContributors(owner: string, repo: string) { + const res = await fetch( + `https://api.github.com/repos/${owner}/${repo}/contributors`, + { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "ora-app", + }, + next: { revalidate: 300 }, // cache for 5 minutes + }, + ); + + if (!res.ok) { + throw new Error(`failed to fetch contributors: ${res.status}`); + } + + const data = await res.json(); + + return data.map((contributor: any) => ({ + name: contributor.login, + avatarUrl: contributor.avatar_url, + contributions: contributor.contributions, + profileUrl: contributor.html_url, + })); +} + export async function getLatestReleaseDmgUrl(owner: string, repo: string) { const res = await fetch( `https://api.github.com/repos/${owner}/${repo}/releases/latest`, diff --git a/src/actions/waitlist.ts b/src/actions/waitlist.ts index cd96437..eceaffe 100644 --- a/src/actions/waitlist.ts +++ b/src/actions/waitlist.ts @@ -17,7 +17,13 @@ export async function addToWaitlist(email: string, ip: string) { } export async function getWaitlistCount() { - const count = await redis.scard("ora:waitlist"); + // const count = await redis.scard("ora:waitlist"); + let count: number; + try { + count = await redis.scard("ora:waitlist"); + } catch { + count = 69 + } return count; } diff --git a/src/components/contributors-list.tsx b/src/components/contributors-list.tsx new file mode 100644 index 0000000..909e5fa --- /dev/null +++ b/src/components/contributors-list.tsx @@ -0,0 +1,22 @@ +import { getRepoContributors } from "@/actions/github"; +import AvatarGroup from "./ui/avatar-group"; + +export async function ContributorsList() { + const contributors = await getRepoContributors("the-ora", "browser"); + + // Transform contributors to match AvatarGroup format + const avatarItems = contributors.map((contributor: any, index: number) => ({ + id: index + 1, + name: contributor.name, + designation: `${contributor.contributions} contributions`, + image: contributor.avatarUrl, + })); + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/footer.tsx b/src/components/footer.tsx index 1f42391..e6cbc67 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -2,6 +2,7 @@ import { PRESENTATION, SOCIALITEMS } from "@/data/presentation"; import { Logo } from "./logo"; import { BuyMeACoffeeBadge } from "./buymecoffe-badge"; import { Button } from "./ui/button"; +import { ContributorsList } from "./contributors-list"; export function Footer() { return ( @@ -29,8 +30,11 @@ export function Footer() { ))} -
- +
+
+ +
+
diff --git a/src/components/github-starts-button.tsx b/src/components/github-stars-button.tsx similarity index 95% rename from src/components/github-starts-button.tsx rename to src/components/github-stars-button.tsx index ca9e778..f9c2222 100644 --- a/src/components/github-starts-button.tsx +++ b/src/components/github-stars-button.tsx @@ -7,7 +7,7 @@ import { Star } from "lucide-react"; import { ArrowRight } from "./animate-ui/icons/arrow-right"; import { formatNumber } from "@/lib/utils"; -export async function GithubStarButton() { +export async function GithubStarsButton() { const stars = await getRepoStars("the-ora", "browser"); return ( diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 08015f0..b5cf323 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -10,46 +10,84 @@ import { getWaitlistCount } from "@/actions/waitlist"; import { getRepoStars } from "@/actions/github"; import { formatNumber } from "@/lib/utils"; import { Button } from "./ui/button"; -import { GithubStarButton } from "./github-starts-button"; +import { GithubStarsButton } from "./github-stars-button"; import { DownloadAlphaButton } from "./download-alpha-button"; import Image from "next/image"; +import { AnimatedGroup } from "./ui/animated-group"; + +const transitionVariants = { + item: { + hidden: { + opacity: 0, + filter: 'blur(12px)', + y: 12, + }, + visible: { + opacity: 1, + filter: 'blur(0px)', + y: 0, + transition: { + type: 'spring' as const, + bounce: 0.3, + duration: 1.5, + }, + }, + }, +} export async function Hero() { const waitlistCount = await getWaitlistCount(); return (
- +
-
- -
+
+ + + +

{PRESENTATION.hero.title}

{PRESENTATION.hero.description}

-
-
+ + -
-
-

- - {formatNumber(waitlistCount)}{" "} - - people have joined the waitlist for beta -

+
+
+
+

+ + {formatNumber(waitlistCount)}{" "} + + people have joined the waitlist for beta +

+
-
-
+ +
Mockup
diff --git a/src/components/ui/animated-group.tsx b/src/components/ui/animated-group.tsx new file mode 100644 index 0000000..53001a7 --- /dev/null +++ b/src/components/ui/animated-group.tsx @@ -0,0 +1,169 @@ +'use client'; +import { ReactNode } from 'react'; +import { motion, Variants } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import React from 'react'; + +type PresetType = + | 'fade' + | 'slide' + | 'scale' + | 'blur' + | 'blur-slide' + | 'zoom' + | 'flip' + | 'bounce' + | 'rotate' + | 'swing'; + +type AnimatedGroupProps = { + children: ReactNode; + className?: string; + variants?: { + container?: Variants; + item?: Variants; + }; + preset?: PresetType; +}; + +const defaultContainerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +const defaultItemVariants: Variants = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, +}; + +const presetVariants: Record< + PresetType, + { container: Variants; item: Variants } +> = { + fade: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }, + }, + slide: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, + }, + }, + scale: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, scale: 0.8 }, + visible: { opacity: 1, scale: 1 }, + }, + }, + blur: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, filter: 'blur(4px)' }, + visible: { opacity: 1, filter: 'blur(0px)' }, + }, + }, + 'blur-slide': { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, filter: 'blur(4px)', y: 20 }, + visible: { opacity: 1, filter: 'blur(0px)', y: 0 }, + }, + }, + zoom: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, scale: 0.5 }, + visible: { + opacity: 1, + scale: 1, + transition: { type: 'spring', stiffness: 300, damping: 20 }, + }, + }, + }, + flip: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, rotateX: -90 }, + visible: { + opacity: 1, + rotateX: 0, + transition: { type: 'spring', stiffness: 300, damping: 20 }, + }, + }, + }, + bounce: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, y: -50 }, + visible: { + opacity: 1, + y: 0, + transition: { type: 'spring', stiffness: 400, damping: 10 }, + }, + }, + }, + rotate: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, rotate: -180 }, + visible: { + opacity: 1, + rotate: 0, + transition: { type: 'spring', stiffness: 200, damping: 15 }, + }, + }, + }, + swing: { + container: defaultContainerVariants, + item: { + hidden: { opacity: 0, rotate: -10 }, + visible: { + opacity: 1, + rotate: 0, + transition: { type: 'spring', stiffness: 300, damping: 8 }, + }, + }, + }, +}; + +function AnimatedGroup({ + children, + className, + variants, + preset, +}: AnimatedGroupProps) { + const selectedVariants = preset + ? presetVariants[preset] + : { container: defaultContainerVariants, item: defaultItemVariants }; + const containerVariants = variants?.container || selectedVariants.container; + const itemVariants = variants?.item || selectedVariants.item; + + return ( + + {React.Children.map(children, (child, index) => ( + + {child} + + ))} + + ); +} + +export { AnimatedGroup }; + diff --git a/src/components/ui/avatar-group.tsx b/src/components/ui/avatar-group.tsx new file mode 100644 index 0000000..b7d0be2 --- /dev/null +++ b/src/components/ui/avatar-group.tsx @@ -0,0 +1,149 @@ +"use client"; +import Image from "next/image"; +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { cn } from "@/lib/utils"; + +interface AvatarItem { + id: number; + name: string; + designation: string; + image: string; +} + +interface AvatarGroupProps { + items: AvatarItem[]; + className?: string; + maxVisible?: number; + size?: "sm" | "md" | "lg"; +} + +// Individual Avatar Component +const Avatar = ({ + item, + index, + totalItems, + size, + isHovered, + onHover, + onLeave, +}: { + item: AvatarItem; + index: number; + totalItems: number; + size: "sm" | "md" | "lg"; + isHovered: boolean; + onHover: () => void; + onLeave: () => void; +}) => { + const sizeClasses = { + sm: "h-8 w-8", + md: "h-10 w-10", + lg: "h-12 w-12", + }; + + return ( +
+ + {isHovered && ( + +
+ {item.name} +
+
+ {item.designation} +
+
+ )} +
+ + + {item.name} + +
+ ); +}; + +const AvatarGroup = ({ + items, + className, + maxVisible = 5, + size = "md", +}: AvatarGroupProps) => { + const [hoveredIndex, setHoveredIndex] = useState(null); + + const visibleItems = items.slice(0, maxVisible); + const remainingCount = items.length - maxVisible; + + return ( +
+ {visibleItems.map((item, index) => ( + setHoveredIndex(item.id)} + onLeave={() => setHoveredIndex(null)} + /> + ))} + + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); +}; + +export default AvatarGroup;