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,569 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useLayoutEffect, useCallback, forwardRef } from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { X, ExternalLink, ChevronLeft, ChevronRight } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
interface Project {
|
|
8
|
+
id: string
|
|
9
|
+
image: string
|
|
10
|
+
title: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AnimatedFolderProps {
|
|
14
|
+
title: string
|
|
15
|
+
projects: Project[]
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AnimatedFolder({ title, projects, className }: AnimatedFolderProps) {
|
|
20
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
21
|
+
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
|
22
|
+
const [sourceRect, setSourceRect] = useState<DOMRect | null>(null)
|
|
23
|
+
const [hiddenCardId, setHiddenCardId] = useState<string | null>(null)
|
|
24
|
+
const cardRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
25
|
+
|
|
26
|
+
const handleProjectClick = (project: Project, index: number) => {
|
|
27
|
+
const cardEl = cardRefs.current[index]
|
|
28
|
+
if (cardEl) {
|
|
29
|
+
setSourceRect(cardEl.getBoundingClientRect())
|
|
30
|
+
}
|
|
31
|
+
setSelectedIndex(index)
|
|
32
|
+
setHiddenCardId(project.id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleCloseLightbox = () => {
|
|
36
|
+
setSelectedIndex(null)
|
|
37
|
+
setSourceRect(null)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handleCloseComplete = () => {
|
|
41
|
+
setHiddenCardId(null)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleNavigate = (newIndex: number) => {
|
|
45
|
+
setSelectedIndex(newIndex)
|
|
46
|
+
setHiddenCardId(projects[newIndex]?.id || null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
"relative flex flex-col items-center justify-center",
|
|
54
|
+
"p-8 rounded-2xl cursor-pointer",
|
|
55
|
+
"bg-card border border-border",
|
|
56
|
+
"transition-all duration-500 ease-out",
|
|
57
|
+
"hover:shadow-2xl hover:shadow-accent/10",
|
|
58
|
+
"hover:border-accent/30",
|
|
59
|
+
"group",
|
|
60
|
+
className,
|
|
61
|
+
)}
|
|
62
|
+
style={{
|
|
63
|
+
minWidth: "100%",
|
|
64
|
+
minHeight: "auto",
|
|
65
|
+
padding: "1rem",
|
|
66
|
+
perspective: "1000px",
|
|
67
|
+
}}
|
|
68
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
69
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
70
|
+
>
|
|
71
|
+
{/* Subtle background glow on hover */}
|
|
72
|
+
<div
|
|
73
|
+
className="absolute inset-0 rounded-2xl transition-opacity duration-500"
|
|
74
|
+
style={{
|
|
75
|
+
background: "radial-gradient(circle at 50% 70%, var(--accent) 0%, transparent 70%)",
|
|
76
|
+
opacity: isHovered ? 0.08 : 0,
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<div className="relative flex items-center justify-center mb-4" style={{ height: "160px", width: "200px" }}>
|
|
81
|
+
{/* Folder back layer - z-index 10 */}
|
|
82
|
+
<div
|
|
83
|
+
className="absolute w-32 h-24 bg-folder-back rounded-lg shadow-md"
|
|
84
|
+
style={{
|
|
85
|
+
transformOrigin: "bottom center",
|
|
86
|
+
transform: isHovered ? "rotateX(-15deg)" : "rotateX(0deg)",
|
|
87
|
+
transition: "transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
88
|
+
zIndex: 10,
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
{/* Folder tab - z-index 10 */}
|
|
93
|
+
<div
|
|
94
|
+
className="absolute w-12 h-4 bg-folder-tab rounded-t-md"
|
|
95
|
+
style={{
|
|
96
|
+
top: "calc(50% - 48px - 12px)",
|
|
97
|
+
left: "calc(50% - 64px + 16px)",
|
|
98
|
+
transformOrigin: "bottom center",
|
|
99
|
+
transform: isHovered ? "rotateX(-25deg) translateY(-2px)" : "rotateX(0deg)",
|
|
100
|
+
transition: "transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
101
|
+
zIndex: 10,
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
{/* Project cards - z-index 20, between back and front */}
|
|
106
|
+
<div
|
|
107
|
+
className="absolute"
|
|
108
|
+
style={{
|
|
109
|
+
top: "50%",
|
|
110
|
+
left: "50%",
|
|
111
|
+
transform: "translate(-50%, -50%)",
|
|
112
|
+
zIndex: 20,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{projects.slice(0, 3).map((project, index) => (
|
|
116
|
+
<ProjectCard
|
|
117
|
+
key={project.id}
|
|
118
|
+
ref={(el) => {
|
|
119
|
+
cardRefs.current[index] = el
|
|
120
|
+
}}
|
|
121
|
+
image={project.image}
|
|
122
|
+
title={project.title}
|
|
123
|
+
delay={index * 80}
|
|
124
|
+
isVisible={isHovered}
|
|
125
|
+
index={index}
|
|
126
|
+
onClick={() => handleProjectClick(project, index)}
|
|
127
|
+
isSelected={hiddenCardId === project.id}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Folder front layer - z-index 30 */}
|
|
133
|
+
<div
|
|
134
|
+
className="absolute w-32 h-24 bg-folder-front rounded-lg shadow-lg"
|
|
135
|
+
style={{
|
|
136
|
+
top: "calc(50% - 48px + 4px)",
|
|
137
|
+
transformOrigin: "bottom center",
|
|
138
|
+
transform: isHovered ? "rotateX(25deg) translateY(8px)" : "rotateX(0deg)",
|
|
139
|
+
transition: "transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
140
|
+
zIndex: 30,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
{/* Folder shine effect - z-index 31 */}
|
|
145
|
+
<div
|
|
146
|
+
className="absolute w-32 h-24 rounded-lg overflow-hidden pointer-events-none"
|
|
147
|
+
style={{
|
|
148
|
+
top: "calc(50% - 48px + 4px)",
|
|
149
|
+
background: "linear-gradient(135deg, rgba(255,255,255,0.3) 0%, transparent 50%)",
|
|
150
|
+
transformOrigin: "bottom center",
|
|
151
|
+
transform: isHovered ? "rotateX(25deg) translateY(8px)" : "rotateX(0deg)",
|
|
152
|
+
transition: "transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
153
|
+
zIndex: 31,
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<ImageLightbox
|
|
160
|
+
projects={projects.slice(0, 3)}
|
|
161
|
+
currentIndex={selectedIndex ?? 0}
|
|
162
|
+
isOpen={selectedIndex !== null}
|
|
163
|
+
onClose={handleCloseLightbox}
|
|
164
|
+
sourceRect={sourceRect}
|
|
165
|
+
onCloseComplete={handleCloseComplete}
|
|
166
|
+
onNavigate={handleNavigate}
|
|
167
|
+
/>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface ImageLightboxProps {
|
|
173
|
+
projects: Project[]
|
|
174
|
+
currentIndex: number
|
|
175
|
+
isOpen: boolean
|
|
176
|
+
onClose: () => void
|
|
177
|
+
sourceRect: DOMRect | null
|
|
178
|
+
onCloseComplete?: () => void
|
|
179
|
+
onNavigate: (index: number) => void
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function ImageLightbox({
|
|
183
|
+
projects,
|
|
184
|
+
currentIndex,
|
|
185
|
+
isOpen,
|
|
186
|
+
onClose,
|
|
187
|
+
sourceRect,
|
|
188
|
+
onCloseComplete,
|
|
189
|
+
onNavigate,
|
|
190
|
+
}: ImageLightboxProps) {
|
|
191
|
+
const [animationPhase, setAnimationPhase] = useState<"initial" | "animating" | "complete">("initial")
|
|
192
|
+
const [isClosing, setIsClosing] = useState(false)
|
|
193
|
+
const [shouldRender, setShouldRender] = useState(false)
|
|
194
|
+
const [internalIndex, setInternalIndex] = useState(currentIndex)
|
|
195
|
+
const [prevIndex, setPrevIndex] = useState(currentIndex)
|
|
196
|
+
const [isSliding, setIsSliding] = useState(false)
|
|
197
|
+
const [slideDirection, setSlideDirection] = useState<"left" | "right">("right")
|
|
198
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
199
|
+
|
|
200
|
+
const totalProjects = projects.length
|
|
201
|
+
const hasNext = internalIndex < totalProjects - 1
|
|
202
|
+
const hasPrev = internalIndex > 0
|
|
203
|
+
|
|
204
|
+
const currentProject = projects[internalIndex]
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (isOpen && currentIndex !== internalIndex && !isSliding) {
|
|
208
|
+
const direction = currentIndex > internalIndex ? "left" : "right"
|
|
209
|
+
setSlideDirection(direction)
|
|
210
|
+
setPrevIndex(internalIndex)
|
|
211
|
+
setIsSliding(true)
|
|
212
|
+
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
setInternalIndex(currentIndex)
|
|
215
|
+
setIsSliding(false)
|
|
216
|
+
}, 400)
|
|
217
|
+
|
|
218
|
+
return () => clearTimeout(timer)
|
|
219
|
+
}
|
|
220
|
+
}, [currentIndex, isOpen])
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (isOpen) {
|
|
224
|
+
setInternalIndex(currentIndex)
|
|
225
|
+
setPrevIndex(currentIndex)
|
|
226
|
+
setIsSliding(false)
|
|
227
|
+
}
|
|
228
|
+
}, [isOpen])
|
|
229
|
+
|
|
230
|
+
const navigateNext = useCallback(() => {
|
|
231
|
+
if (internalIndex >= totalProjects - 1 || isSliding) return
|
|
232
|
+
onNavigate(internalIndex + 1)
|
|
233
|
+
}, [internalIndex, totalProjects, isSliding, onNavigate])
|
|
234
|
+
|
|
235
|
+
const navigatePrev = useCallback(() => {
|
|
236
|
+
if (internalIndex <= 0 || isSliding) return
|
|
237
|
+
onNavigate(internalIndex - 1)
|
|
238
|
+
}, [internalIndex, isSliding, onNavigate])
|
|
239
|
+
|
|
240
|
+
const handleClose = useCallback(() => {
|
|
241
|
+
setIsClosing(true)
|
|
242
|
+
onClose()
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
setIsClosing(false)
|
|
245
|
+
setShouldRender(false)
|
|
246
|
+
setAnimationPhase("initial")
|
|
247
|
+
onCloseComplete?.()
|
|
248
|
+
}, 400)
|
|
249
|
+
}, [onClose, onCloseComplete])
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
253
|
+
if (!isOpen) return
|
|
254
|
+
if (e.key === "Escape") handleClose()
|
|
255
|
+
if (e.key === "ArrowRight") navigateNext()
|
|
256
|
+
if (e.key === "ArrowLeft") navigatePrev()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
window.addEventListener("keydown", handleKeyDown)
|
|
260
|
+
if (isOpen) {
|
|
261
|
+
document.body.style.overflow = "hidden"
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return () => {
|
|
265
|
+
window.removeEventListener("keydown", handleKeyDown)
|
|
266
|
+
document.body.style.overflow = ""
|
|
267
|
+
}
|
|
268
|
+
}, [isOpen, handleClose, navigateNext, navigatePrev])
|
|
269
|
+
|
|
270
|
+
useLayoutEffect(() => {
|
|
271
|
+
if (isOpen && sourceRect) {
|
|
272
|
+
setShouldRender(true)
|
|
273
|
+
setAnimationPhase("initial")
|
|
274
|
+
setIsClosing(false)
|
|
275
|
+
requestAnimationFrame(() => {
|
|
276
|
+
requestAnimationFrame(() => {
|
|
277
|
+
setAnimationPhase("animating")
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
const timer = setTimeout(() => {
|
|
281
|
+
setAnimationPhase("complete")
|
|
282
|
+
}, 500)
|
|
283
|
+
return () => clearTimeout(timer)
|
|
284
|
+
}
|
|
285
|
+
}, [isOpen, sourceRect])
|
|
286
|
+
|
|
287
|
+
const handleDotClick = (idx: number) => {
|
|
288
|
+
if (isSliding || idx === internalIndex) return
|
|
289
|
+
onNavigate(idx)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!shouldRender || !currentProject) return null
|
|
293
|
+
|
|
294
|
+
const getInitialStyles = (): React.CSSProperties => {
|
|
295
|
+
if (!sourceRect) return {}
|
|
296
|
+
|
|
297
|
+
const viewportWidth = window.innerWidth
|
|
298
|
+
const viewportHeight = window.innerHeight
|
|
299
|
+
const targetWidth = Math.min(768, viewportWidth - 64)
|
|
300
|
+
const targetHeight = Math.min(viewportHeight * 0.85, 600)
|
|
301
|
+
|
|
302
|
+
const targetX = (viewportWidth - targetWidth) / 2
|
|
303
|
+
const targetY = (viewportHeight - targetHeight) / 2
|
|
304
|
+
|
|
305
|
+
const scaleX = sourceRect.width / targetWidth
|
|
306
|
+
const scaleY = sourceRect.height / targetHeight
|
|
307
|
+
const scale = Math.max(scaleX, scaleY)
|
|
308
|
+
|
|
309
|
+
const translateX = sourceRect.left + sourceRect.width / 2 - (targetX + targetWidth / 2)
|
|
310
|
+
const translateY = sourceRect.top + sourceRect.height / 2 - (targetY + targetHeight / 2)
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
|
|
314
|
+
opacity: 1,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const getFinalStyles = (): React.CSSProperties => {
|
|
319
|
+
return {
|
|
320
|
+
transform: "translate(0, 0) scale(1)",
|
|
321
|
+
opacity: 1,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const currentStyles = animationPhase === "initial" && !isClosing ? getInitialStyles() : getFinalStyles()
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<div
|
|
329
|
+
className={cn("fixed inset-0 z-50 flex items-center justify-center p-4 md:p-8")}
|
|
330
|
+
onClick={handleClose}
|
|
331
|
+
style={{
|
|
332
|
+
opacity: isClosing ? 0 : 1,
|
|
333
|
+
transition: "opacity 400ms cubic-bezier(0.16, 1, 0.3, 1)",
|
|
334
|
+
}}
|
|
335
|
+
>
|
|
336
|
+
<div
|
|
337
|
+
className="absolute inset-0 bg-background/80 backdrop-blur-xl"
|
|
338
|
+
style={{
|
|
339
|
+
opacity: animationPhase === "initial" && !isClosing ? 0 : 1,
|
|
340
|
+
transition: "opacity 400ms cubic-bezier(0.16, 1, 0.3, 1)",
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
|
|
344
|
+
{/* Close button */}
|
|
345
|
+
<button
|
|
346
|
+
onClick={(e) => {
|
|
347
|
+
e.stopPropagation()
|
|
348
|
+
handleClose()
|
|
349
|
+
}}
|
|
350
|
+
className={cn(
|
|
351
|
+
"absolute top-5 right-5 z-50",
|
|
352
|
+
"w-10 h-10 flex items-center justify-center",
|
|
353
|
+
"rounded-full bg-muted/50 backdrop-blur-md",
|
|
354
|
+
"border border-border",
|
|
355
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
356
|
+
"transition-all duration-300 ease-out hover:scale-105 active:scale-95",
|
|
357
|
+
)}
|
|
358
|
+
style={{
|
|
359
|
+
opacity: animationPhase === "complete" && !isClosing ? 1 : 0,
|
|
360
|
+
transform: animationPhase === "complete" && !isClosing ? "translateY(0)" : "translateY(-10px)",
|
|
361
|
+
transition: "opacity 300ms ease-out, transform 300ms ease-out",
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
<X className="w-4 h-4" strokeWidth={2.5} />
|
|
365
|
+
</button>
|
|
366
|
+
|
|
367
|
+
<button
|
|
368
|
+
onClick={(e) => {
|
|
369
|
+
e.stopPropagation()
|
|
370
|
+
navigatePrev()
|
|
371
|
+
}}
|
|
372
|
+
disabled={!hasPrev || isSliding}
|
|
373
|
+
className={cn(
|
|
374
|
+
"absolute left-4 md:left-8 z-50",
|
|
375
|
+
"w-12 h-12 flex items-center justify-center",
|
|
376
|
+
"rounded-full bg-muted/50 backdrop-blur-md",
|
|
377
|
+
"border border-border",
|
|
378
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
379
|
+
"transition-all duration-300 ease-out hover:scale-110 active:scale-95",
|
|
380
|
+
"disabled:opacity-0 disabled:pointer-events-none",
|
|
381
|
+
)}
|
|
382
|
+
style={{
|
|
383
|
+
opacity: animationPhase === "complete" && !isClosing && hasPrev ? 1 : 0,
|
|
384
|
+
transform: animationPhase === "complete" && !isClosing ? "translateX(0)" : "translateX(-20px)",
|
|
385
|
+
transition: "opacity 300ms ease-out 150ms, transform 300ms ease-out 150ms",
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<ChevronLeft className="w-5 h-5" strokeWidth={2.5} />
|
|
389
|
+
</button>
|
|
390
|
+
|
|
391
|
+
<button
|
|
392
|
+
onClick={(e) => {
|
|
393
|
+
e.stopPropagation()
|
|
394
|
+
navigateNext()
|
|
395
|
+
}}
|
|
396
|
+
disabled={!hasNext || isSliding}
|
|
397
|
+
className={cn(
|
|
398
|
+
"absolute right-4 md:right-8 z-50",
|
|
399
|
+
"w-12 h-12 flex items-center justify-center",
|
|
400
|
+
"rounded-full bg-muted/50 backdrop-blur-md",
|
|
401
|
+
"border border-border",
|
|
402
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
403
|
+
"transition-all duration-300 ease-out hover:scale-110 active:scale-95",
|
|
404
|
+
"disabled:opacity-0 disabled:pointer-events-none",
|
|
405
|
+
)}
|
|
406
|
+
style={{
|
|
407
|
+
opacity: animationPhase === "complete" && !isClosing && hasNext ? 1 : 0,
|
|
408
|
+
transform: animationPhase === "complete" && !isClosing ? "translateX(0)" : "translateX(20px)",
|
|
409
|
+
transition: "opacity 300ms ease-out 150ms, transform 300ms ease-out 150ms",
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
<ChevronRight className="w-5 h-5" strokeWidth={2.5} />
|
|
413
|
+
</button>
|
|
414
|
+
|
|
415
|
+
<div
|
|
416
|
+
ref={containerRef}
|
|
417
|
+
className="relative z-10 w-full max-w-[95vw]"
|
|
418
|
+
onClick={(e) => e.stopPropagation()}
|
|
419
|
+
style={{
|
|
420
|
+
...currentStyles,
|
|
421
|
+
transform: isClosing ? "translate(0, 0) scale(0.95)" : currentStyles.transform,
|
|
422
|
+
transition:
|
|
423
|
+
animationPhase === "initial" && !isClosing
|
|
424
|
+
? "none"
|
|
425
|
+
: "transform 400ms cubic-bezier(0.16, 1, 0.3, 1), opacity 400ms ease-out",
|
|
426
|
+
transformOrigin: "center center",
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
<div
|
|
430
|
+
className={cn("relative overflow-hidden", "rounded-2xl", "bg-card", "ring-1 ring-border", "shadow-2xl")}
|
|
431
|
+
style={{
|
|
432
|
+
borderRadius: animationPhase === "initial" && !isClosing ? "8px" : "16px",
|
|
433
|
+
transition: "border-radius 500ms cubic-bezier(0.16, 1, 0.3, 1)",
|
|
434
|
+
}}
|
|
435
|
+
>
|
|
436
|
+
<div className="relative overflow-hidden">
|
|
437
|
+
<div
|
|
438
|
+
className="flex transition-transform duration-400 ease-out"
|
|
439
|
+
style={{
|
|
440
|
+
transform: `translateX(-${internalIndex * 100}%)`,
|
|
441
|
+
transition: isSliding ? "transform 400ms cubic-bezier(0.32, 0.72, 0, 1)" : "none",
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
{projects.map((project, idx) => (
|
|
445
|
+
<img
|
|
446
|
+
key={project.id}
|
|
447
|
+
src={project.image || "/placeholder.svg"}
|
|
448
|
+
alt={project.title}
|
|
449
|
+
className="w-full h-auto max-h-[85vh] object-contain bg-background flex-shrink-0"
|
|
450
|
+
style={{ minWidth: "100%" }}
|
|
451
|
+
/>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{/* Subtle vignette effect */}
|
|
456
|
+
<div className="absolute inset-0 pointer-events-none bg-gradient-to-t from-card/20 via-transparent to-card/10" />
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div
|
|
460
|
+
className={cn("px-6 py-5", "bg-card", "border-t border-border")}
|
|
461
|
+
style={{
|
|
462
|
+
opacity: animationPhase === "complete" && !isClosing ? 1 : 0,
|
|
463
|
+
transform: animationPhase === "complete" && !isClosing ? "translateY(0)" : "translateY(20px)",
|
|
464
|
+
transition: "opacity 300ms ease-out 100ms, transform 300ms ease-out 100ms",
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
<div className="flex items-start justify-between gap-4">
|
|
468
|
+
<div className="flex-1 min-w-0">
|
|
469
|
+
<h3 className="text-lg font-medium text-foreground tracking-tight truncate h-7">
|
|
470
|
+
{currentProject?.title}
|
|
471
|
+
</h3>
|
|
472
|
+
<div className="flex items-center gap-3 mt-1">
|
|
473
|
+
<p className="text-sm text-muted-foreground">
|
|
474
|
+
<kbd className="px-1.5 py-0.5 mx-0.5 text-xs font-medium bg-muted text-muted-foreground rounded border border-border">
|
|
475
|
+
←
|
|
476
|
+
</kbd>
|
|
477
|
+
<kbd className="px-1.5 py-0.5 mx-0.5 text-xs font-medium bg-muted text-muted-foreground rounded border border-border">
|
|
478
|
+
→
|
|
479
|
+
</kbd>{" "}
|
|
480
|
+
to navigate
|
|
481
|
+
</p>
|
|
482
|
+
<div className="flex items-center gap-1.5">
|
|
483
|
+
{projects.map((_, idx) => (
|
|
484
|
+
<button
|
|
485
|
+
key={idx}
|
|
486
|
+
onClick={() => handleDotClick(idx)}
|
|
487
|
+
className={cn(
|
|
488
|
+
"w-2 h-2 rounded-full transition-all duration-300",
|
|
489
|
+
idx === internalIndex
|
|
490
|
+
? "bg-foreground scale-110"
|
|
491
|
+
: "bg-muted-foreground/40 hover:bg-muted-foreground/60",
|
|
492
|
+
)}
|
|
493
|
+
/>
|
|
494
|
+
))}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<button
|
|
500
|
+
className={cn(
|
|
501
|
+
"flex items-center gap-2 px-4 py-2",
|
|
502
|
+
"text-sm font-medium text-muted-foreground",
|
|
503
|
+
"bg-muted/50 hover:bg-muted",
|
|
504
|
+
"rounded-lg border border-border",
|
|
505
|
+
"transition-all duration-200 ease-out",
|
|
506
|
+
"hover:text-foreground",
|
|
507
|
+
)}
|
|
508
|
+
>
|
|
509
|
+
<span>View</span>
|
|
510
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
511
|
+
</button>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
interface ProjectCardProps {
|
|
521
|
+
image: string
|
|
522
|
+
title: string
|
|
523
|
+
delay: number
|
|
524
|
+
isVisible: boolean
|
|
525
|
+
index: number
|
|
526
|
+
onClick: () => void
|
|
527
|
+
isSelected: boolean
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export const ProjectCard = forwardRef<HTMLDivElement, ProjectCardProps>(
|
|
531
|
+
({ image, title, delay, isVisible, index, onClick, isSelected }, ref) => {
|
|
532
|
+
const rotations = [-12, 0, 12]
|
|
533
|
+
const translations = [-55, 0, 55]
|
|
534
|
+
|
|
535
|
+
return (
|
|
536
|
+
<div
|
|
537
|
+
ref={ref}
|
|
538
|
+
className={cn(
|
|
539
|
+
"absolute w-20 h-28 rounded-lg overflow-hidden shadow-xl",
|
|
540
|
+
"bg-card border border-border",
|
|
541
|
+
"cursor-pointer hover:ring-2 hover:ring-accent/50",
|
|
542
|
+
isSelected && "opacity-0",
|
|
543
|
+
)}
|
|
544
|
+
style={{
|
|
545
|
+
transform: isVisible
|
|
546
|
+
? `translateY(-90px) translateX(${translations[index]}px) rotate(${rotations[index]}deg) scale(1)`
|
|
547
|
+
: "translateY(0px) translateX(0px) rotate(0deg) scale(0.5)",
|
|
548
|
+
opacity: isSelected ? 0 : isVisible ? 1 : 0,
|
|
549
|
+
transition: `all 600ms cubic-bezier(0.34, 1.56, 0.64, 1) ${delay}ms`,
|
|
550
|
+
zIndex: 10 - index,
|
|
551
|
+
left: "-40px",
|
|
552
|
+
top: "-56px",
|
|
553
|
+
}}
|
|
554
|
+
onClick={(e) => {
|
|
555
|
+
e.stopPropagation()
|
|
556
|
+
onClick()
|
|
557
|
+
}}
|
|
558
|
+
>
|
|
559
|
+
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-full object-cover" />
|
|
560
|
+
<div className="absolute inset-0 bg-gradient-to-t from-foreground/60 to-transparent" />
|
|
561
|
+
<p className="absolute bottom-1.5 left-1.5 right-1.5 text-[10px] font-medium text-foreground truncate">
|
|
562
|
+
{title}
|
|
563
|
+
</p>
|
|
564
|
+
</div>
|
|
565
|
+
)
|
|
566
|
+
},
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
ProjectCard.displayName = "ProjectCard"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const Avatar = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<AvatarPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn(
|
|
15
|
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
))
|
|
21
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
22
|
+
|
|
23
|
+
const AvatarImage = React.forwardRef<
|
|
24
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
25
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
26
|
+
>(({ className, ...props }, ref) => (
|
|
27
|
+
<AvatarPrimitive.Image
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn("aspect-square h-full w-full", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
))
|
|
33
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
34
|
+
|
|
35
|
+
const AvatarFallback = React.forwardRef<
|
|
36
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
37
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
38
|
+
>(({ className, ...props }, ref) => (
|
|
39
|
+
<AvatarPrimitive.Fallback
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={cn(
|
|
42
|
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
))
|
|
48
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
49
|
+
|
|
50
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
17
|
+
outline: "text-foreground",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export interface BadgeProps
|
|
27
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
28
|
+
VariantProps<typeof badgeVariants> { }
|
|
29
|
+
|
|
30
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
2
|
+
|
|
3
|
+
const AvatarDemo = () => {
|
|
4
|
+
return (
|
|
5
|
+
<Avatar>
|
|
6
|
+
<AvatarImage src='https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-5.png' alt='Hallie Richards' />
|
|
7
|
+
<AvatarFallback className='text-xs'>HR</AvatarFallback>
|
|
8
|
+
</Avatar>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default AvatarDemo
|