abhishek-portfolio-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/bin/cli.js +54 -0
- package/package.json +27 -0
- package/template/components.json +22 -0
- package/template/next.config.ts +79 -0
- package/template/package.json +43 -0
- package/template/postcss.config.js +6 -0
- package/template/public/BoliviaSignature-ZpWnz.ttf +0 -0
- package/template/public/Gemini_Generated_Image_xc97toxc97toxc97.png +0 -0
- package/template/public/Hendrigo.otf +0 -0
- package/template/public/audiomass-output.mp3 +0 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/googlec77e59474f5a09cb.html +1 -0
- package/template/public/icon-192x192.png +0 -0
- package/template/public/icon-512x512.png +0 -0
- package/template/public/next.svg +1 -0
- package/template/public/paper sound .mpeg +0 -0
- package/template/public/removebg.png +0 -0
- package/template/public/resume.pdf +0 -0
- package/template/public/sw.js +1 -0
- package/template/public/swe-worker-5c72df51bb1f6ee0.js +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/public/workbox-f1770938.js +1 -0
- package/template/src/app/about/page.tsx +91 -0
- package/template/src/app/actions/optimize-text.ts +54 -0
- package/template/src/app/gaming/page.tsx +308 -0
- package/template/src/app/github/[username]/page.tsx +97 -0
- package/template/src/app/globals.css +321 -0
- package/template/src/app/layout.tsx +39 -0
- package/template/src/app/manifest.ts +25 -0
- package/template/src/app/not-found.tsx +16 -0
- package/template/src/app/page.tsx +28 -0
- package/template/src/app/robots.ts +12 -0
- package/template/src/app/sitemap.ts +38 -0
- package/template/src/app/template.tsx +5 -0
- package/template/src/app/works/[slug]/page.tsx +50 -0
- package/template/src/app/works/client.tsx +44 -0
- package/template/src/app/works/page.tsx +24 -0
- package/template/src/components/about/about-client.tsx +259 -0
- package/template/src/components/home/bento-gallery.tsx +52 -0
- package/template/src/components/home/contact-section.tsx +34 -0
- package/template/src/components/home/craft-card.tsx +18 -0
- package/template/src/components/home/featured-projects.tsx +186 -0
- package/template/src/components/home/focus-card.tsx +171 -0
- package/template/src/components/home/identity-card.tsx +45 -0
- package/template/src/components/home/philosophy-card.tsx +104 -0
- package/template/src/components/home/skills-in-motion.tsx +109 -0
- package/template/src/components/home/tech-stack-marquee.tsx +56 -0
- package/template/src/components/ui/3d-folder.tsx +569 -0
- package/template/src/components/ui/avatar.tsx +50 -0
- package/template/src/components/ui/badge.tsx +36 -0
- package/template/src/components/ui/basic-avatar.tsx +12 -0
- package/template/src/components/ui/button.tsx +117 -0
- package/template/src/components/ui/clipboard-secret.tsx +39 -0
- package/template/src/components/ui/command-menu.tsx +519 -0
- package/template/src/components/ui/command-palette.tsx +152 -0
- package/template/src/components/ui/consciousness-mode.tsx +200 -0
- package/template/src/components/ui/copy-code-button.tsx +135 -0
- package/template/src/components/ui/display-cards.tsx +70 -0
- package/template/src/components/ui/dotted-map.tsx +128 -0
- package/template/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/src/components/ui/emoji-rating.tsx +123 -0
- package/template/src/components/ui/exit-message.tsx +50 -0
- package/template/src/components/ui/image-zoom-overlay.tsx +178 -0
- package/template/src/components/ui/input-otp.tsx +71 -0
- package/template/src/components/ui/kbd.tsx +87 -0
- package/template/src/components/ui/location-tag.tsx +232 -0
- package/template/src/components/ui/minimal-testimonial.tsx +97 -0
- package/template/src/components/ui/mobile-menu.tsx +191 -0
- package/template/src/components/ui/navbar.tsx +148 -0
- package/template/src/components/ui/page-transition.tsx +24 -0
- package/template/src/components/ui/pixeleted-404-not-found.tsx +110 -0
- package/template/src/components/ui/preloader-wrapper.tsx +102 -0
- package/template/src/components/ui/preloader.tsx +104 -0
- package/template/src/components/ui/project-contributors.tsx +57 -0
- package/template/src/components/ui/scroll-area.tsx +117 -0
- package/template/src/components/ui/signature.tsx +173 -0
- package/template/src/components/ui/smooth-scroll.tsx +31 -0
- package/template/src/components/ui/social-icons.tsx +103 -0
- package/template/src/components/ui/social-stories.tsx +394 -0
- package/template/src/components/ui/sound-constants.ts +1 -0
- package/template/src/components/ui/text-explode.tsx +188 -0
- package/template/src/components/ui/toast.tsx +80 -0
- package/template/src/components/ui/tooltip.tsx +30 -0
- package/template/src/components/ui/user-location.tsx +151 -0
- package/template/src/components/ui/vertical-image-stack.tsx +345 -0
- package/template/src/components/works/changelog-overlay.tsx +212 -0
- package/template/src/components/works/currently-working-card.tsx +130 -0
- package/template/src/components/works/project-details-view.tsx +464 -0
- package/template/src/components/works/project-grid.tsx +81 -0
- package/template/src/fonts/BoliviaSignature-ZpWnz.ttf +0 -0
- package/template/src/fonts/Hendrigo.otf +0 -0
- package/template/src/lib/data.ts +61 -0
- package/template/src/lib/fonts.ts +14 -0
- package/template/src/lib/github.ts +15 -0
- package/template/src/lib/supabase.ts +11 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/tailwind.config.ts +31 -0
- package/template/tsconfig.json +34 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const kbdVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center font-mono font-medium text-xs bg-muted text-muted-foreground border border-border rounded-md border-b-3 transition-all duration-75 cursor-pointer select-none active:translate-y-[1px] active:border-b-[1px] hover:bg-muted/80 shadow-sm/2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-muted text-muted-foreground border-border",
|
|
14
|
+
outline:
|
|
15
|
+
"bg-transparent border-border text-foreground hover:bg-accent",
|
|
16
|
+
solid:
|
|
17
|
+
"bg-foreground text-background border-foreground hover:bg-foreground/90",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground border-border hover:bg-secondary/80",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
xs: "h-5 px-1.5 text-[10px] min-w-[1.25rem]",
|
|
23
|
+
sm: "h-6 px-2 text-xs min-w-[1.5rem]",
|
|
24
|
+
md: "h-7 px-2.5 text-sm min-w-[1.75rem]",
|
|
25
|
+
lg: "h-8 px-3 text-sm min-w-[2rem]",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "sm",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export interface KbdProps
|
|
36
|
+
extends React.HTMLAttributes<HTMLElement>,
|
|
37
|
+
VariantProps<typeof kbdVariants> {
|
|
38
|
+
keys?: string[];
|
|
39
|
+
onClick?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
43
|
+
({ className, variant, size, keys, children, onClick, ...props }, ref) => {
|
|
44
|
+
// If keys array is provided, render multiple kbd elements
|
|
45
|
+
if (keys && keys.length > 0) {
|
|
46
|
+
return (
|
|
47
|
+
<span
|
|
48
|
+
className="inline-flex items-center gap-1"
|
|
49
|
+
ref={ref as React.Ref<HTMLSpanElement>}
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
>
|
|
52
|
+
{keys.map((key, index) => (
|
|
53
|
+
<React.Fragment key={index}>
|
|
54
|
+
<kbd
|
|
55
|
+
className={cn(kbdVariants({ variant, size }), className)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{key}
|
|
59
|
+
</kbd>
|
|
60
|
+
{index < keys.length - 1 && (
|
|
61
|
+
<span className="text-muted-foreground text-xs px-1">
|
|
62
|
+
+
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</React.Fragment>
|
|
66
|
+
))}
|
|
67
|
+
</span>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Single kbd element
|
|
72
|
+
return (
|
|
73
|
+
<kbd
|
|
74
|
+
className={cn(kbdVariants({ variant, size }), className)}
|
|
75
|
+
ref={ref}
|
|
76
|
+
onClick={onClick}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</kbd>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
Kbd.displayName = "Kbd";
|
|
86
|
+
|
|
87
|
+
export { Kbd, kbdVariants };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
+
import { supabase } from "@/lib/supabase"
|
|
6
|
+
|
|
7
|
+
interface LocationTagProps {
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function LocationTag({ className = "" }: LocationTagProps) {
|
|
12
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
13
|
+
const [currentTime, setCurrentTime] = useState("")
|
|
14
|
+
const [location, setLocation] = useState({ city: "Kolkata", country: "India", timezone: "Asia/Kolkata" })
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// Fetch location data
|
|
18
|
+
const fetchLocation = async () => {
|
|
19
|
+
const { data } = await supabase.from("profile").select("location_city, location_country, location_timezone").single();
|
|
20
|
+
if (data) {
|
|
21
|
+
setLocation({
|
|
22
|
+
city: data.location_city || "Kolkata",
|
|
23
|
+
country: data.location_country || "India",
|
|
24
|
+
timezone: data.location_timezone || "Asia/Kolkata"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
fetchLocation();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const updateTime = () => {
|
|
33
|
+
try {
|
|
34
|
+
const now = new Date()
|
|
35
|
+
setCurrentTime(
|
|
36
|
+
now.toLocaleTimeString("en-US", {
|
|
37
|
+
hour: "2-digit",
|
|
38
|
+
minute: "2-digit",
|
|
39
|
+
hour12: false,
|
|
40
|
+
timeZone: location.timezone,
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Fallback for invalid timezone
|
|
45
|
+
setCurrentTime("00:00")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
updateTime()
|
|
49
|
+
const interval = setInterval(updateTime, 1000)
|
|
50
|
+
return () => clearInterval(interval)
|
|
51
|
+
}, [location.timezone])
|
|
52
|
+
|
|
53
|
+
const [isRetro, setIsRetro] = useState(false);
|
|
54
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
55
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
56
|
+
const [terminalLines, setTerminalLines] = useState<string[]>([]);
|
|
57
|
+
const [terminalColor, setTerminalColor] = useState<'gray' | 'green'>('gray');
|
|
58
|
+
const pressTimer = useRef<NodeJS.Timeout | null>(null);
|
|
59
|
+
|
|
60
|
+
// Initialize state from localStorage
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const storedRetro = localStorage.getItem('retro-mode') === 'true';
|
|
63
|
+
if (storedRetro) {
|
|
64
|
+
setIsRetro(true);
|
|
65
|
+
document.documentElement.classList.add('retro-mode');
|
|
66
|
+
}
|
|
67
|
+
setIsInitialized(true);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
// Sync state with localStorage and DOM
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!isInitialized) return;
|
|
73
|
+
|
|
74
|
+
if (isRetro) {
|
|
75
|
+
document.documentElement.classList.add('retro-mode');
|
|
76
|
+
localStorage.setItem('retro-mode', 'true');
|
|
77
|
+
} else {
|
|
78
|
+
document.documentElement.classList.remove('retro-mode');
|
|
79
|
+
localStorage.setItem('retro-mode', 'false');
|
|
80
|
+
}
|
|
81
|
+
}, [isRetro, isInitialized]);
|
|
82
|
+
|
|
83
|
+
// Manage terminal-active class for Navbar visibility
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (showOverlay) {
|
|
86
|
+
document.documentElement.classList.add('terminal-active');
|
|
87
|
+
} else {
|
|
88
|
+
document.documentElement.classList.remove('terminal-active');
|
|
89
|
+
}
|
|
90
|
+
return () => {
|
|
91
|
+
document.documentElement.classList.remove('terminal-active');
|
|
92
|
+
}
|
|
93
|
+
}, [showOverlay]);
|
|
94
|
+
|
|
95
|
+
const triggerRetroSequence = async () => {
|
|
96
|
+
if (isRetro) {
|
|
97
|
+
setIsRetro(false);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setShowOverlay(true);
|
|
102
|
+
setTerminalLines([]);
|
|
103
|
+
setTerminalColor('gray');
|
|
104
|
+
|
|
105
|
+
if (typeof navigator !== "undefined" && navigator.vibrate) navigator.vibrate(50);
|
|
106
|
+
|
|
107
|
+
const addLine = async (line: string, delay: number) => {
|
|
108
|
+
setTerminalLines(prev => [...prev, line]);
|
|
109
|
+
if (typeof navigator !== "undefined" && navigator.vibrate) navigator.vibrate(10);
|
|
110
|
+
await new Promise(r => setTimeout(r, delay));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// DESTRUCTION PHASE (Gray)
|
|
114
|
+
await new Promise(r => setTimeout(r, 500));
|
|
115
|
+
|
|
116
|
+
await addLine('> SYSTEM_ALERT: UNEXPECTED_INTERACTION', 400);
|
|
117
|
+
await addLine('> ROOT_QUERY: "Why did you touch that?"', 800);
|
|
118
|
+
await addLine('> WARNING: "Restricted Access Protocol"', 600);
|
|
119
|
+
await addLine("> ROOT_ACCESS_GRANTED", 400);
|
|
120
|
+
await addLine("> EXECUTING: sudo rm -rf /portfolio_v2", 800);
|
|
121
|
+
await addLine("> PURGING_ASSETS_DIRECTORY...", 400);
|
|
122
|
+
await addLine("> UNLINKING_STYLESHEETS...", 400);
|
|
123
|
+
await addLine("> CLEARING_RUNTIME_CACHE...", 600);
|
|
124
|
+
|
|
125
|
+
if (typeof navigator !== "undefined" && navigator.vibrate) navigator.vibrate([50, 50, 100]);
|
|
126
|
+
await addLine("> PROCESS_COMPLETE: CURRENT_VIEW_TERMINATED", 1200);
|
|
127
|
+
|
|
128
|
+
// REBIRTH PHASE (Green)
|
|
129
|
+
setTerminalColor('green');
|
|
130
|
+
await addLine("> INITIALIZING_LEGACY_PROTOCOL...", 800);
|
|
131
|
+
await addLine("> LOADING_BACKUP_ARCHIVES...", 500);
|
|
132
|
+
await addLine("> CHECKING_VRAM... [OK]", 400);
|
|
133
|
+
await addLine("> LAUNCHING_LEGACY_INTERFACE...", 1500);
|
|
134
|
+
|
|
135
|
+
// Activate Retro Mode
|
|
136
|
+
// Activate Retro Mode
|
|
137
|
+
setIsRetro(true);
|
|
138
|
+
|
|
139
|
+
setShowOverlay(false);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handlePressStart = (e: React.MouseEvent | React.TouchEvent) => {
|
|
143
|
+
pressTimer.current = setTimeout(() => {
|
|
144
|
+
triggerRetroSequence();
|
|
145
|
+
}, 800);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handlePressEnd = () => {
|
|
149
|
+
if (pressTimer.current) clearTimeout(pressTimer.current);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
{/* Terminal Overlay */}
|
|
155
|
+
<AnimatePresence>
|
|
156
|
+
{showOverlay && (
|
|
157
|
+
<motion.div
|
|
158
|
+
initial={{ opacity: 0 }}
|
|
159
|
+
animate={{ opacity: 1 }}
|
|
160
|
+
exit={{ opacity: 0 }}
|
|
161
|
+
transition={{ duration: 0.2 }}
|
|
162
|
+
className={`fixed inset-0 z-[100000] bg-black p-6 md:p-20 font-mono overflow-hidden cursor-wait flex flex-col justify-start items-start transition-colors duration-500 ${terminalColor === 'green' ? 'text-[#33ff00] drop-shadow-[0_0_8px_rgba(51,255,0,0.4)]' : 'text-gray-300'}`}
|
|
163
|
+
>
|
|
164
|
+
<div className="w-full max-w-4xl space-y-1 text-xs md:text-lg uppercase">
|
|
165
|
+
{terminalLines.map((line, i) => (
|
|
166
|
+
<motion.div
|
|
167
|
+
key={i}
|
|
168
|
+
initial={{ opacity: 0, x: -10 }}
|
|
169
|
+
animate={{ opacity: 1, x: 0 }}
|
|
170
|
+
className="break-all md:break-words whitespace-pre-wrap"
|
|
171
|
+
>
|
|
172
|
+
{line}
|
|
173
|
+
</motion.div>
|
|
174
|
+
))}
|
|
175
|
+
|
|
176
|
+
{/* Blinking Cursor */}
|
|
177
|
+
<motion.div
|
|
178
|
+
initial={{ opacity: 0 }}
|
|
179
|
+
animate={{ opacity: 1 }}
|
|
180
|
+
className="mt-2"
|
|
181
|
+
>
|
|
182
|
+
<span className={terminalColor === 'green' ? 'text-[#33ff00]' : 'text-gray-500'}>{'>'}</span>
|
|
183
|
+
<motion.span
|
|
184
|
+
animate={{ opacity: [0, 1, 0] }}
|
|
185
|
+
transition={{ duration: 0.8, repeat: Infinity }}
|
|
186
|
+
className={`inline-block w-2 h-4 md:w-3 md:h-5 ml-2 align-middle ${terminalColor === 'green' ? 'bg-[#33ff00]' : 'bg-gray-300'}`}
|
|
187
|
+
/>
|
|
188
|
+
</motion.div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Scanline Effect */}
|
|
192
|
+
<div className="absolute inset-0 pointer-events-none select-none opacity-10" style={{ background: "repeating-linear-gradient(0deg, transparent, transparent 2px, #ffffff 4px)" }}></div>
|
|
193
|
+
</motion.div>
|
|
194
|
+
)}
|
|
195
|
+
</AnimatePresence>
|
|
196
|
+
|
|
197
|
+
<button
|
|
198
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
199
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
200
|
+
onMouseDown={handlePressStart}
|
|
201
|
+
onMouseUp={handlePressEnd}
|
|
202
|
+
onTouchStart={handlePressStart}
|
|
203
|
+
onTouchEnd={handlePressEnd}
|
|
204
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
205
|
+
style={{ WebkitTouchCallout: 'none' }}
|
|
206
|
+
className={`group relative flex items-center gap-3 rounded-full border border-white/10 bg-white/5 px-4 py-2 transition-all duration-300 hover:border-white/20 hover:bg-white/10 select-none touch-none ${className}`}
|
|
207
|
+
>
|
|
208
|
+
<div className="relative flex items-center justify-center">
|
|
209
|
+
<span className={`absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 ${isRetro ? 'bg-amber-500' : 'bg-emerald-500'}`} />
|
|
210
|
+
<span className={`relative inline-flex h-2 w-2 rounded-full ${isRetro ? 'bg-amber-500' : 'bg-emerald-500'}`} />
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div className="relative overflow-hidden h-5 flex flex-col justify-center">
|
|
216
|
+
<AnimatePresence mode="wait">
|
|
217
|
+
<motion.span
|
|
218
|
+
key={isRetro ? "retro" : "standard"}
|
|
219
|
+
initial={{ y: 20, opacity: 0 }}
|
|
220
|
+
animate={{ y: 0, opacity: 1 }}
|
|
221
|
+
exit={{ y: -20, opacity: 0 }}
|
|
222
|
+
transition={{ duration: 0.3 }}
|
|
223
|
+
className={`text-sm font-medium block ${isRetro ? 'font-mono text-amber-500' : 'font-sans text-white/90'}`}
|
|
224
|
+
>
|
|
225
|
+
{isRetro ? "SYSTEM OVERRIDE" : "Available for hire"}
|
|
226
|
+
</motion.span>
|
|
227
|
+
</AnimatePresence>
|
|
228
|
+
</div>
|
|
229
|
+
</button>
|
|
230
|
+
</>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import Image from "next/image"
|
|
5
|
+
|
|
6
|
+
const testimonials = [
|
|
7
|
+
{
|
|
8
|
+
quote: "Working with them transformed our entire brand identity. The attention to detail was exceptional.",
|
|
9
|
+
name: "Sarah Chen",
|
|
10
|
+
role: "CEO at Stripe",
|
|
11
|
+
image: "https://images.unsplash.com/photo-1701615004837-40d8573b6652?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDB8fGF2YXRhcnN8ZW58MHx8MHx8fDA%3D$0",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
quote: "A rare talent who combines strategic thinking with flawless execution. Highly recommended.",
|
|
15
|
+
name: "Marcus Johnson",
|
|
16
|
+
role: "Design Lead at Linear",
|
|
17
|
+
image: "https://images.unsplash.com/photo-1639149888905-fb39731f2e6c?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDN8fGF2YXRhcnN8ZW58MHx8MHx8fDA%3D$0",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
quote: "The most seamless collaboration I've experienced. They truly understand modern design.",
|
|
21
|
+
name: "Elena Voss",
|
|
22
|
+
role: "Founder at Notion",
|
|
23
|
+
image: "https://plus.unsplash.com/premium_photo-1689977830819-d00b3a9b7363?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NTJ8fGF2YXRhcnN8ZW58MHx8MHx8fDA%3D$0",
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export function TestimonialsMinimal() {
|
|
28
|
+
const [active, setActive] = useState(0)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="w-full max-w-4xl mx-auto px-6 py-24 md:py-32">
|
|
32
|
+
{/* Section Header */}
|
|
33
|
+
<div className="mb-16 md:mb-20 text-center">
|
|
34
|
+
<h2 className="text-sm font-medium text-blue-400 tracking-wider uppercase mb-3">Testimonials</h2>
|
|
35
|
+
<h3 className="text-3xl md:text-4xl font-semibold text-white">What Clients Say</h3>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Quote using CSS Grid for auto-height stacking */}
|
|
39
|
+
<div className="relative mb-12 grid grid-cols-1 min-h-[120px]">
|
|
40
|
+
{testimonials.map((t, i) => (
|
|
41
|
+
<div
|
|
42
|
+
key={i}
|
|
43
|
+
className={`
|
|
44
|
+
col-start-1 row-start-1
|
|
45
|
+
text-2xl md:text-4xl font-light leading-snug text-white/90
|
|
46
|
+
transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]
|
|
47
|
+
${active === i
|
|
48
|
+
? "opacity-100 translate-y-0 scale-100 blur-0 z-10"
|
|
49
|
+
: "opacity-0 translate-y-8 scale-95 blur-sm z-0 pointer-events-none"
|
|
50
|
+
}
|
|
51
|
+
`}
|
|
52
|
+
>
|
|
53
|
+
"{t.quote}"
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Author Row */}
|
|
59
|
+
<div className="flex flex-col md:flex-row items-start md:items-center gap-6 md:gap-8 pt-6 border-t border-white/10">
|
|
60
|
+
{/* Avatars */}
|
|
61
|
+
<div className="flex -space-x-3">
|
|
62
|
+
{testimonials.map((t, i) => (
|
|
63
|
+
<button
|
|
64
|
+
key={i}
|
|
65
|
+
onClick={() => setActive(i)}
|
|
66
|
+
className={`
|
|
67
|
+
relative w-12 h-12 md:w-14 md:h-14 rounded-full overflow-hidden ring-4 ring-[#0a0a0a]
|
|
68
|
+
transition-all duration-500 ease-out
|
|
69
|
+
${active === i ? "z-10 scale-110 grayscale-0" : "grayscale opacity-50 hover:opacity-100 hover:scale-105 hover:grayscale-0"}
|
|
70
|
+
`}
|
|
71
|
+
>
|
|
72
|
+
<Image src={t.image || "/placeholder.svg"} alt={t.name} fill className="object-cover" />
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Active Author Info */}
|
|
78
|
+
<div className="flex-1 grid grid-cols-1">
|
|
79
|
+
{testimonials.map((t, i) => (
|
|
80
|
+
<div
|
|
81
|
+
key={i}
|
|
82
|
+
className={`
|
|
83
|
+
col-start-1 row-start-1
|
|
84
|
+
flex flex-col justify-center
|
|
85
|
+
transition-all duration-500 ease-out
|
|
86
|
+
${active === i ? "opacity-100 translate-x-0 z-10" : "opacity-0 -translate-x-4 pointer-events-none z-0"}
|
|
87
|
+
`}
|
|
88
|
+
>
|
|
89
|
+
<span className="text-lg font-medium text-white">{t.name}</span>
|
|
90
|
+
<span className="text-sm text-white/50">{t.role}</span>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion, AnimatePresence, Variants, useMotionValue, useTransform, animate } from "framer-motion";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
|
|
8
|
+
// import { SocialStories } from "@/components/ui/social-stories";
|
|
9
|
+
// import TextExplode from "./text-explode";
|
|
10
|
+
import { X } from "lucide-react";
|
|
11
|
+
|
|
12
|
+
interface MobileMenuProps {
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const menuVariants: Variants = {
|
|
18
|
+
closed: {
|
|
19
|
+
y: "100%",
|
|
20
|
+
transition: {
|
|
21
|
+
duration: 0.5,
|
|
22
|
+
ease: [0.32, 0, 0.67, 0]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
open: {
|
|
26
|
+
y: "0%",
|
|
27
|
+
transition: {
|
|
28
|
+
duration: 0.6,
|
|
29
|
+
ease: [0.22, 1, 0.36, 1]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const linkVariants: Variants = {
|
|
35
|
+
closed: { opacity: 0, scale: 0.95, y: 10 },
|
|
36
|
+
open: (i: number) => ({
|
|
37
|
+
opacity: 1,
|
|
38
|
+
scale: 1,
|
|
39
|
+
y: 0,
|
|
40
|
+
transition: {
|
|
41
|
+
delay: 0.15 + i * 0.08,
|
|
42
|
+
duration: 0.5,
|
|
43
|
+
ease: [0.22, 1, 0.36, 1]
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const LINKS = ["Home", "Works", "About"];
|
|
49
|
+
|
|
50
|
+
export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
|
|
51
|
+
const pathname = usePathname();
|
|
52
|
+
const router = useRouter();
|
|
53
|
+
const y = useMotionValue(0);
|
|
54
|
+
|
|
55
|
+
// Prefetch Home on mount for instant load after animation
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
router.prefetch("/");
|
|
58
|
+
}, [router]);
|
|
59
|
+
|
|
60
|
+
// Hard-Resistance Transform: "Stretch very slowly" -> Heavy linear resistance
|
|
61
|
+
// raw drag value -> visual stretch value
|
|
62
|
+
const stretch = useTransform(y, (v: any) => {
|
|
63
|
+
const num = typeof v === 'string' ? parseFloat(v) : v;
|
|
64
|
+
const rawY = -(num || 0);
|
|
65
|
+
if (rawY <= 0) return 0;
|
|
66
|
+
|
|
67
|
+
// Linear heavy resistance: moves 1px for every 4px of finger drag
|
|
68
|
+
return rawY * 0.25;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Thresholds adjusted for early reveal
|
|
72
|
+
const textOpacity = useTransform(stretch, [2, 15], [0, 1]);
|
|
73
|
+
const textScale = useTransform(stretch, [2, 15], [0.9, 1]);
|
|
74
|
+
const barHeight = useTransform(stretch, [0, 40], [0, 80]);
|
|
75
|
+
|
|
76
|
+
// Removed explosion logic entirely
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<AnimatePresence>
|
|
80
|
+
{isOpen && (
|
|
81
|
+
<>
|
|
82
|
+
{/* Backdrop */}
|
|
83
|
+
<motion.div
|
|
84
|
+
initial={{ opacity: 0 }}
|
|
85
|
+
animate={{ opacity: 1 }}
|
|
86
|
+
exit={{ opacity: 0 }}
|
|
87
|
+
onClick={onClose}
|
|
88
|
+
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[90]"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
{/* Bottom Sheet */}
|
|
92
|
+
<motion.div
|
|
93
|
+
style={{ y }}
|
|
94
|
+
variants={menuVariants}
|
|
95
|
+
initial="closed"
|
|
96
|
+
animate="open"
|
|
97
|
+
exit="closed"
|
|
98
|
+
drag="y"
|
|
99
|
+
dragConstraints={{ top: -85, bottom: 0 }} // Limit expansion to just show the text
|
|
100
|
+
dragElastic={0.05} // Slight elasticity at the limit so it doesn't feel broken
|
|
101
|
+
onDragEnd={(_, info) => {
|
|
102
|
+
if (info.offset.y > 85 || info.velocity.y > 500) {
|
|
103
|
+
onClose();
|
|
104
|
+
} else {
|
|
105
|
+
// Snappy return to simulate rubber band Snap
|
|
106
|
+
animate(y, 0, {
|
|
107
|
+
type: "tween",
|
|
108
|
+
ease: [0.33, 1, 0.68, 1], // easeOutQuart
|
|
109
|
+
duration: 0.4
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
className="fixed bottom-0 left-0 right-0 bg-[#111111] border-t border-white/10 rounded-t-[32px] p-8 z-[100] pb-12 cursor-grab active:cursor-grabbing shadow-[0_-30px_60px_rgba(0,0,0,0.6)]"
|
|
114
|
+
>
|
|
115
|
+
{/* Visually Infinite Background - Massive filler */}
|
|
116
|
+
<div className="absolute top-[60%] left-0 right-0 h-[800vh] bg-[#111111] -z-10" />
|
|
117
|
+
|
|
118
|
+
{/* Close Indicator */}
|
|
119
|
+
<div className="flex justify-center mb-6">
|
|
120
|
+
<div className="w-12 h-1.5 bg-white/20 rounded-full" />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Spacer or Alternative Content - Removed SocialStories as per user request */}
|
|
124
|
+
<div className="mb-8" />
|
|
125
|
+
|
|
126
|
+
{/* Links - Clean vertical stack */}
|
|
127
|
+
<div className="flex flex-col gap-8 items-center justify-center min-h-[200px]">
|
|
128
|
+
{LINKS.map((item, i) => (
|
|
129
|
+
<motion.div
|
|
130
|
+
key={item}
|
|
131
|
+
custom={i}
|
|
132
|
+
variants={linkVariants}
|
|
133
|
+
initial="closed"
|
|
134
|
+
animate="open"
|
|
135
|
+
>
|
|
136
|
+
<Link
|
|
137
|
+
href={item === "Home" ? "/" : `/${item.toLowerCase()}`}
|
|
138
|
+
onClick={(e) => {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
onClose();
|
|
141
|
+
const href = item === "Home" ? "/" : `/${item.toLowerCase()}`;
|
|
142
|
+
|
|
143
|
+
if (pathname === href) {
|
|
144
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
145
|
+
} else {
|
|
146
|
+
// Smooth Transition: Close menu (0.4s) -> Then Navigate
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
router.push(href);
|
|
149
|
+
}, 400);
|
|
150
|
+
}
|
|
151
|
+
}}
|
|
152
|
+
className="text-4xl font-medium text-white/90 hover:text-white transition-colors tracking-tight"
|
|
153
|
+
>
|
|
154
|
+
{item}
|
|
155
|
+
</Link>
|
|
156
|
+
</motion.div>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Spacer to push content up slightly */}
|
|
161
|
+
<div className="h-16" />
|
|
162
|
+
|
|
163
|
+
{/* Easter Egg Bottom Zone - Static Text with Apple Emoji */}
|
|
164
|
+
<motion.div
|
|
165
|
+
style={{ height: barHeight }}
|
|
166
|
+
className="absolute bottom-0 left-0 right-0 flex items-end justify-center overflow-hidden pointer-events-none"
|
|
167
|
+
>
|
|
168
|
+
<motion.div
|
|
169
|
+
style={{
|
|
170
|
+
opacity: textOpacity,
|
|
171
|
+
scale: textScale,
|
|
172
|
+
}}
|
|
173
|
+
className="px-4 pb-4 flex items-center gap-2" // Anchored to the very bottom
|
|
174
|
+
>
|
|
175
|
+
<span className="text-lg md:text-xl font-bold text-white/50 whitespace-nowrap">
|
|
176
|
+
Why are you stretching that !
|
|
177
|
+
</span>
|
|
178
|
+
{/* Apple-style Broken Heart Emoji via CDN to ensure Windows assumes it's Apple */}
|
|
179
|
+
<img
|
|
180
|
+
src="https://emojicdn.elk.sh/🤨?style=apple"
|
|
181
|
+
alt="sus"
|
|
182
|
+
className="w-6 h-6 mb-0.5"
|
|
183
|
+
/>
|
|
184
|
+
</motion.div>
|
|
185
|
+
</motion.div>
|
|
186
|
+
</motion.div>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
</AnimatePresence>
|
|
190
|
+
);
|
|
191
|
+
}
|