Skip to content

Commit b869936

Browse files
adjust color and let the goats fly.... (#2313)
* adjust color and let the goats fly.... * Update components/HiringBadge.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix dark mode --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 09e140c commit b869936

File tree

2 files changed

+111
-19
lines changed

2 files changed

+111
-19
lines changed

components/HiringBadge.tsx

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,113 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, useRef } from "react";
44
import Link from "next/link";
55
import { cn } from "@/lib/utils";
66
import { buttonVariants } from "./ui/button";
77

88
export function HiringBadge() {
99
const [isHovered, setIsHovered] = useState(false);
10+
const [goats, setGoats] = useState<Array<{ id: number; x: number; y: number; delay: number; duration: number }>>([]);
11+
const containerRef = useRef<HTMLDivElement>(null);
12+
const goatIdRef = useRef(0);
13+
const lastSpawnTimeRef = useRef(0);
14+
15+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
16+
if (!containerRef.current) return;
17+
const rect = containerRef.current.getBoundingClientRect();
18+
const x = e.clientX - rect.left;
19+
const y = e.clientY - rect.top;
20+
21+
// Throttle goat spawning (spawn every 100ms)
22+
const now = Date.now();
23+
if (isHovered && now - lastSpawnTimeRef.current > 100) {
24+
lastSpawnTimeRef.current = now;
25+
26+
const newGoat = {
27+
id: goatIdRef.current++,
28+
x,
29+
y,
30+
delay: 0,
31+
duration: 1.5 + (Math.random() * 0.6),
32+
};
33+
setGoats((prev) => [...prev, newGoat]);
34+
35+
// Remove goat after animation completes
36+
setTimeout(() => {
37+
setGoats((prev) => prev.filter((goat) => goat.id !== newGoat.id));
38+
}, (newGoat.duration + 0.5) * 1000);
39+
}
40+
};
41+
42+
const handleMouseEnter = () => {
43+
setIsHovered(true);
44+
};
45+
46+
const handleMouseLeave = () => {
47+
setIsHovered(false);
48+
setGoats([]);
49+
};
1050

1151
return (
12-
<Link
13-
href="/careers"
14-
className={cn(
15-
buttonVariants({ variant: "cta", size: "pill" }),
16-
"hidden lg:inline-flex h-6 px-2.5 text-[11px] font-medium"
17-
)}
18-
onMouseEnter={() => setIsHovered(true)}
19-
onMouseLeave={() => setIsHovered(false)}
20-
onFocus={() => setIsHovered(true)}
21-
onBlur={() => setIsHovered(false)}
22-
style={{ width: "max-content" }}
52+
<div
53+
ref={containerRef}
54+
className="relative hidden lg:block"
55+
onMouseMove={handleMouseMove}
56+
onMouseEnter={handleMouseEnter}
57+
onMouseLeave={handleMouseLeave}
2358
>
24-
<span className={cn(isHovered && "invisible")}>
25-
Hiring in Berlin and SF
26-
</span>
27-
<span className={cn("absolute", !isHovered && "invisible")}>
28-
Looking for 🐐s!
29-
</span>
30-
</Link>
59+
<Link
60+
href="/careers"
61+
className={cn(
62+
buttonVariants({ variant: "outline", size: "pill" }),
63+
"inline-flex h-6 px-2.5 text-[11px] font-medium bg-white dark:bg-muted text-foreground hover:bg-accent/50 dark:hover:bg-muted/80 items-center gap-1.5 relative z-10"
64+
)}
65+
onFocus={() => setIsHovered(true)}
66+
onBlur={() => setIsHovered(false)}
67+
style={{ width: "max-content" }}
68+
>
69+
<span className="w-1.5 h-1.5 rounded-full bg-[theme(colors.orange.400)] shrink-0" />
70+
<span className="relative">
71+
<span className={cn("block", isHovered && "invisible")}>
72+
Hiring in Berlin and SF
73+
</span>
74+
<span className={cn("absolute left-0 top-0", !isHovered && "invisible")}>
75+
Looking for GOATS!
76+
</span>
77+
</span>
78+
</Link>
79+
80+
{/* Falling goats animation */}
81+
{isHovered && (
82+
<div className="fixed pointer-events-none overflow-visible z-50" style={{
83+
top: 0,
84+
left: 0,
85+
width: "100vw",
86+
height: "100vh",
87+
}}>
88+
{goats.map((goat) => {
89+
if (!containerRef.current) return null;
90+
const rect = containerRef.current.getBoundingClientRect();
91+
const absoluteX = rect.left + goat.x;
92+
const absoluteY = rect.top + goat.y;
93+
94+
return (
95+
<span
96+
key={goat.id}
97+
className="absolute text-lg animate-fall"
98+
style={{
99+
left: `${absoluteX}px`,
100+
top: `${absoluteY}px`,
101+
animationDelay: `${goat.delay}s`,
102+
animationDuration: `${goat.duration}s`,
103+
}}
104+
>
105+
🐐
106+
</span>
107+
);
108+
})}
109+
</div>
110+
)}
111+
</div>
31112
);
32113
}

tailwind.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ module.exports = {
7777
marquee: "marquee var(--duration) linear infinite",
7878
grid: "grid 20s linear infinite",
7979
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
80+
fall: "fall 1.5s linear forwards",
8081
},
8182
keyframes: {
8283
"accordion-down": {
@@ -134,6 +135,16 @@ module.exports = {
134135
transform: "translate(calc(100cqw - 100%), 0)",
135136
},
136137
},
138+
fall: {
139+
"0%": {
140+
transform: "translateY(0px) rotate(0deg)",
141+
opacity: "1",
142+
},
143+
"100%": {
144+
transform: "translateY(250px) rotate(360deg)",
145+
opacity: "0",
146+
},
147+
},
137148
},
138149
perspective: {
139150
'1000': '1000px',

0 commit comments

Comments
 (0)