From 20c45eea5d02140c7e85b268fa28c12120de4d26 Mon Sep 17 00:00:00 2001 From: FormalSnake Date: Sat, 20 Sep 2025 10:26:31 +0100 Subject: [PATCH 1/8] fix(redis): Handle potential errors when getting waitlist count --- src/actions/waitlist.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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; } From 5166794fc22ba8a16124793bfd5eee3a5ddbe62d Mon Sep 17 00:00:00 2001 From: FormalSnake Date: Sat, 20 Sep 2025 10:51:00 +0100 Subject: [PATCH 2/8] Add GitHub contributors list and update hero section --- next.config.ts | 8 +++++ src/actions/github.ts | 26 ++++++++++++++ src/components/contributors-list.tsx | 36 +++++++++++++++++++ ...rts-button.tsx => github-stars-button.tsx} | 2 +- src/components/hero.tsx | 12 ++++--- 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/components/contributors-list.tsx rename src/components/{github-starts-button.tsx => github-stars-button.tsx} (95%) 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/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/components/contributors-list.tsx b/src/components/contributors-list.tsx new file mode 100644 index 0000000..0403b07 --- /dev/null +++ b/src/components/contributors-list.tsx @@ -0,0 +1,36 @@ +import { getRepoContributors } from "@/actions/github"; +import Image from "next/image"; +import Link from "next/link"; + +interface Contributor { + name: string; + avatarUrl: string; + contributions: number; + profileUrl: string; +} + +export async function ContributorsList() { + const contributors = await getRepoContributors("the-ora", "browser"); + + return ( +
+ {contributors.map((contributor: Contributor) => ( + + {contributor.name} + + ))} +
+ ); +} \ No newline at end of file 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..fd20fab 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -10,19 +10,20 @@ 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 { ContributorsList } from "./contributors-list"; export async function Hero() { const waitlistCount = await getWaitlistCount(); return (
- +
-
- +
+

{PRESENTATION.hero.title} @@ -43,13 +44,14 @@ export async function Hero() {

+
Mockup
From a0e90fe663dd44edee0da0206d5f49fc6c5dda2d Mon Sep 17 00:00:00 2001 From: FormalSnake Date: Sat, 20 Sep 2025 11:04:28 +0100 Subject: [PATCH 3/8] Add animated group component for hero section --- src/components/hero.tsx | 62 +++++++--- src/components/ui/animated-group.tsx | 169 +++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 src/components/ui/animated-group.tsx diff --git a/src/components/hero.tsx b/src/components/hero.tsx index fd20fab..20c1165 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -14,6 +14,27 @@ import { GithubStarsButton } from "./github-stars-button"; import { DownloadAlphaButton } from "./download-alpha-button"; import Image from "next/image"; import { ContributorsList } from "./contributors-list"; +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', + bounce: 0.3, + duration: 1.5, + }, + }, + }, +} export async function Hero() { const waitlistCount = await getWaitlistCount(); @@ -24,27 +45,42 @@ export async function Hero() {
-
+

{PRESENTATION.hero.title}

{PRESENTATION.hero.description}

-
-
+ + -
-
-

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

+
+
+
+

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

+
-
- + + {/* */}
= { + 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 }; + From 49dbf517a8f417952d062bc10bdb64f8fcb83475 Mon Sep 17 00:00:00 2001 From: FormalSnake Date: Sat, 20 Sep 2025 11:06:15 +0100 Subject: [PATCH 4/8] feat(src/components): Add AnimatedGroup component to hero section --- src/components/hero.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 20c1165..92d4088 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -44,7 +44,9 @@ export async function Hero() {
- + + +

{PRESENTATION.hero.title} From fcf5689e27525f4e7f74cae872de1a28c7d182e0 Mon Sep 17 00:00:00 2001 From: FormalSnake Date: Sat, 20 Sep 2025 11:11:09 +0100 Subject: [PATCH 5/8] feat: Update hero component image blur effect --- src/components/hero.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 92d4088..4d37775 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -83,7 +83,7 @@ export async function Hero() {

{/* */} -
+
Mockup Date: Sat, 20 Sep 2025 11:15:59 +0100 Subject: [PATCH 6/8] Update dependencies and add framer-motion component --- bun.lock | 27 ++--- package.json | 3 +- src/components/contributors-list.tsx | 42 +++----- src/components/footer.tsx | 8 +- src/components/hero.tsx | 1 - src/components/ui/avatar-group.tsx | 149 +++++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 44 deletions(-) create mode 100644 src/components/ui/avatar-group.tsx 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/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/components/contributors-list.tsx b/src/components/contributors-list.tsx index 0403b07..4f0c689 100644 --- a/src/components/contributors-list.tsx +++ b/src/components/contributors-list.tsx @@ -1,36 +1,22 @@ import { getRepoContributors } from "@/actions/github"; -import Image from "next/image"; -import Link from "next/link"; - -interface Contributor { - name: string; - avatarUrl: string; - contributions: number; - profileUrl: string; -} +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, index) => ({ + id: index + 1, + name: contributor.name, + designation: `${contributor.contributions} contributions`, + image: contributor.avatarUrl, + })); + return ( -
- {contributors.map((contributor: Contributor) => ( - - {contributor.name} - - ))} -
+ ); } \ 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/hero.tsx b/src/components/hero.tsx index 4d37775..0230fb0 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -13,7 +13,6 @@ import { Button } from "./ui/button"; import { GithubStarsButton } from "./github-stars-button"; import { DownloadAlphaButton } from "./download-alpha-button"; import Image from "next/image"; -import { ContributorsList } from "./contributors-list"; import { AnimatedGroup } from "./ui/animated-group"; const transitionVariants = { 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; From 21b63d86fe16af95fae639c3b9d862e7bd7c6061 Mon Sep 17 00:00:00 2001 From: FormalSnake Date: Sat, 20 Sep 2025 11:16:52 +0100 Subject: [PATCH 7/8] remove ContributorsList component from Hero component --- src/components/hero.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 0230fb0..64ae00d 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -81,7 +81,6 @@ export async function Hero() {
- {/* */}
Date: Sat, 20 Sep 2025 11:18:56 +0100 Subject: [PATCH 8/8] feat(contributors): Update avatarItems mapping and hero.tsx transition type --- src/components/contributors-list.tsx | 2 +- src/components/hero.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/contributors-list.tsx b/src/components/contributors-list.tsx index 4f0c689..909e5fa 100644 --- a/src/components/contributors-list.tsx +++ b/src/components/contributors-list.tsx @@ -5,7 +5,7 @@ export async function ContributorsList() { const contributors = await getRepoContributors("the-ora", "browser"); // Transform contributors to match AvatarGroup format - const avatarItems = contributors.map((contributor, index) => ({ + const avatarItems = contributors.map((contributor: any, index: number) => ({ id: index + 1, name: contributor.name, designation: `${contributor.contributions} contributions`, diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 64ae00d..b5cf323 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -27,7 +27,7 @@ const transitionVariants = { filter: 'blur(0px)', y: 0, transition: { - type: 'spring', + type: 'spring' as const, bounce: 0.3, duration: 1.5, },