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,200 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
5
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
12
|
+
|
|
13
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
14
|
+
|
|
15
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
16
|
+
|
|
17
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
18
|
+
|
|
19
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
20
|
+
|
|
21
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
22
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
24
|
+
inset?: boolean
|
|
25
|
+
}
|
|
26
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
27
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
|
31
|
+
inset && "pl-8",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
38
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
39
|
+
))
|
|
40
|
+
DropdownMenuSubTrigger.displayName =
|
|
41
|
+
DropdownMenuPrimitive.SubTrigger.displayName
|
|
42
|
+
|
|
43
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
44
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
45
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
46
|
+
>(({ className, ...props }, ref) => (
|
|
47
|
+
<DropdownMenuPrimitive.SubContent
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn(
|
|
50
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
))
|
|
56
|
+
DropdownMenuSubContent.displayName =
|
|
57
|
+
DropdownMenuPrimitive.SubContent.displayName
|
|
58
|
+
|
|
59
|
+
const DropdownMenuContent = React.forwardRef<
|
|
60
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
61
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
62
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
63
|
+
<DropdownMenuPrimitive.Portal>
|
|
64
|
+
<DropdownMenuPrimitive.Content
|
|
65
|
+
ref={ref}
|
|
66
|
+
sideOffset={sideOffset}
|
|
67
|
+
className={cn(
|
|
68
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
</DropdownMenuPrimitive.Portal>
|
|
74
|
+
))
|
|
75
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
76
|
+
|
|
77
|
+
const DropdownMenuItem = React.forwardRef<
|
|
78
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
79
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
80
|
+
inset?: boolean
|
|
81
|
+
}
|
|
82
|
+
>(({ className, inset, ...props }, ref) => (
|
|
83
|
+
<DropdownMenuPrimitive.Item
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn(
|
|
86
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
87
|
+
inset && "pl-8",
|
|
88
|
+
className,
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
))
|
|
93
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
94
|
+
|
|
95
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
96
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
97
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
98
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
99
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
100
|
+
ref={ref}
|
|
101
|
+
className={cn(
|
|
102
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
checked={checked}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
109
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
110
|
+
<Check className="h-4 w-4" />
|
|
111
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
112
|
+
</span>
|
|
113
|
+
{children}
|
|
114
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
115
|
+
))
|
|
116
|
+
DropdownMenuCheckboxItem.displayName =
|
|
117
|
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
118
|
+
|
|
119
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
120
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
121
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
122
|
+
>(({ className, children, ...props }, ref) => (
|
|
123
|
+
<DropdownMenuPrimitive.RadioItem
|
|
124
|
+
ref={ref}
|
|
125
|
+
className={cn(
|
|
126
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
132
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
133
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
134
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
135
|
+
</span>
|
|
136
|
+
{children}
|
|
137
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
138
|
+
))
|
|
139
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
140
|
+
|
|
141
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
142
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
143
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
144
|
+
inset?: boolean
|
|
145
|
+
}
|
|
146
|
+
>(({ className, inset, ...props }, ref) => (
|
|
147
|
+
<DropdownMenuPrimitive.Label
|
|
148
|
+
ref={ref}
|
|
149
|
+
className={cn(
|
|
150
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
151
|
+
inset && "pl-8",
|
|
152
|
+
className,
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
))
|
|
157
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
158
|
+
|
|
159
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
160
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
161
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
162
|
+
>(({ className, ...props }, ref) => (
|
|
163
|
+
<DropdownMenuPrimitive.Separator
|
|
164
|
+
ref={ref}
|
|
165
|
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
166
|
+
{...props}
|
|
167
|
+
/>
|
|
168
|
+
))
|
|
169
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
170
|
+
|
|
171
|
+
const DropdownMenuShortcut = ({
|
|
172
|
+
className,
|
|
173
|
+
...props
|
|
174
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
175
|
+
return (
|
|
176
|
+
<span
|
|
177
|
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
178
|
+
{...props}
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
DropdownMenu,
|
|
186
|
+
DropdownMenuTrigger,
|
|
187
|
+
DropdownMenuContent,
|
|
188
|
+
DropdownMenuItem,
|
|
189
|
+
DropdownMenuCheckboxItem,
|
|
190
|
+
DropdownMenuRadioItem,
|
|
191
|
+
DropdownMenuLabel,
|
|
192
|
+
DropdownMenuSeparator,
|
|
193
|
+
DropdownMenuShortcut,
|
|
194
|
+
DropdownMenuGroup,
|
|
195
|
+
DropdownMenuPortal,
|
|
196
|
+
DropdownMenuSub,
|
|
197
|
+
DropdownMenuSubContent,
|
|
198
|
+
DropdownMenuSubTrigger,
|
|
199
|
+
DropdownMenuRadioGroup,
|
|
200
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
interface RatingInteractionProps {
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ratingData = [
|
|
11
|
+
{ emoji: "😔", label: "Terrible", color: "from-red-400 to-red-500", shadowColor: "shadow-red-500/30" },
|
|
12
|
+
{ emoji: "😕", label: "Poor", color: "from-orange-400 to-orange-500", shadowColor: "shadow-orange-500/30" },
|
|
13
|
+
{ emoji: "😐", label: "Okay", color: "from-yellow-400 to-yellow-500", shadowColor: "shadow-yellow-500/30" },
|
|
14
|
+
{ emoji: "🙂", label: "Good", color: "from-lime-400 to-lime-500", shadowColor: "shadow-lime-500/30" },
|
|
15
|
+
{ emoji: "😍", label: "Amazing", color: "from-emerald-400 to-emerald-500", shadowColor: "shadow-emerald-500/30" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function RatingInteraction({ className }: RatingInteractionProps) {
|
|
19
|
+
const [rating, setRating] = useState(0)
|
|
20
|
+
const [hoverRating, setHoverRating] = useState(0)
|
|
21
|
+
const [submitted, setSubmitted] = useState(false)
|
|
22
|
+
|
|
23
|
+
const handleClick = async (value: number) => {
|
|
24
|
+
setRating(value)
|
|
25
|
+
setSubmitted(true)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await fetch('/api/ratings', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ rating: value })
|
|
32
|
+
})
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error("Failed to submit rating", e)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const displayRating = hoverRating || rating
|
|
39
|
+
|
|
40
|
+
if (submitted) {
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn("flex flex-col items-center gap-4 py-8 animate-in fade-in zoom-in duration-500", className)}>
|
|
43
|
+
<div className="w-16 h-16 mb-2"><img src="https://emojicdn.elk.sh/🎉?style=apple" className="w-full h-full object-contain" alt="Success" /></div>
|
|
44
|
+
<p className="text-white/60 font-medium">Thank you for your feedback!</p>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={cn("flex flex-col items-center gap-6", className)}>
|
|
51
|
+
<div className="text-center space-y-1">
|
|
52
|
+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-white/40">How was your experience?</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Emoji rating buttons */}
|
|
56
|
+
<div className="flex items-center gap-3">
|
|
57
|
+
{ratingData.map((item, i) => {
|
|
58
|
+
const value = i + 1
|
|
59
|
+
const isActive = value <= displayRating
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
key={value}
|
|
64
|
+
onClick={() => handleClick(value)}
|
|
65
|
+
onMouseEnter={() => setHoverRating(value)}
|
|
66
|
+
onMouseLeave={() => setHoverRating(0)}
|
|
67
|
+
className="group relative focus:outline-none"
|
|
68
|
+
aria-label={`Rate ${value}: ${item.label}`}
|
|
69
|
+
>
|
|
70
|
+
<div
|
|
71
|
+
className={cn(
|
|
72
|
+
"relative flex h-12 w-12 md:h-14 md:w-14 items-center justify-center rounded-2xl transition-all duration-300 ease-out bg-white/5 border border-white/5",
|
|
73
|
+
isActive ? "scale-110 bg-white/10" : "scale-100 group-hover:scale-105 group-hover:bg-white/10",
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{/* Emoji with smooth grayscale transition */}
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
"w-8 h-8 md:w-10 md:h-10 transition-all duration-300 ease-out select-none flex items-center justify-center",
|
|
80
|
+
isActive
|
|
81
|
+
? "grayscale-0 drop-shadow-lg scale-110"
|
|
82
|
+
: "grayscale opacity-40 group-hover:opacity-100 group-hover:grayscale-0",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<img
|
|
86
|
+
src={`https://emojicdn.elk.sh/${item.emoji}?style=apple`}
|
|
87
|
+
alt={item.label}
|
|
88
|
+
className="w-full h-full object-contain pointer-events-none"
|
|
89
|
+
loading="lazy"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</button>
|
|
94
|
+
)
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="relative h-6 w-32">
|
|
99
|
+
{/* Rating labels */}
|
|
100
|
+
{ratingData.map((item, i) => (
|
|
101
|
+
<div
|
|
102
|
+
key={i}
|
|
103
|
+
className={cn(
|
|
104
|
+
"absolute inset-0 flex items-center justify-center transition-all duration-300 ease-out",
|
|
105
|
+
displayRating === i + 1 ? "opacity-100 blur-0 scale-100" : "opacity-0 blur-md scale-105",
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<span className="text-xs font-medium tracking-wide text-white/60">{item.label}</span>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
{/* Default Empty State Text (Optional, or just hide) */}
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
"absolute inset-0 flex items-center justify-center transition-all duration-300 ease-out",
|
|
115
|
+
displayRating === 0 ? "opacity-100 blur-0 scale-100" : "opacity-0 blur-md scale-95",
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<span className="text-xs font-medium tracking-wide text-white/20">Select one</span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
|
|
6
|
+
export const ExitMessage = () => {
|
|
7
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const handleMouseLeave = (e: MouseEvent) => {
|
|
11
|
+
// Check if cursor left from the top (Exit Intent)
|
|
12
|
+
if (e.clientY <= 0) {
|
|
13
|
+
setIsVisible(true);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const handleMouseEnter = () => {
|
|
18
|
+
// Hide if user comes back
|
|
19
|
+
setIsVisible(false);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
document.documentElement.addEventListener("mouseleave", handleMouseLeave);
|
|
23
|
+
document.documentElement.addEventListener("mouseenter", handleMouseEnter);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
document.documentElement.removeEventListener("mouseleave", handleMouseLeave);
|
|
27
|
+
document.documentElement.removeEventListener("mouseenter", handleMouseEnter);
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<AnimatePresence>
|
|
33
|
+
{isVisible && (
|
|
34
|
+
<motion.div
|
|
35
|
+
initial={{ opacity: 0, y: 20 }}
|
|
36
|
+
animate={{ opacity: 1, y: 0 }}
|
|
37
|
+
exit={{ opacity: 0, y: 20 }}
|
|
38
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
39
|
+
className="fixed bottom-0 left-0 w-full flex justify-center pb-6 pointer-events-none z-[10000]"
|
|
40
|
+
>
|
|
41
|
+
<div className="bg-black/50 backdrop-blur-md border border-white/5 rounded-full px-6 py-2 shadow-2xl">
|
|
42
|
+
<p className="text-white/60 text-xs font-medium tracking-widest uppercase">
|
|
43
|
+
Thanks for spending time here.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
</motion.div>
|
|
47
|
+
)}
|
|
48
|
+
</AnimatePresence>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion";
|
|
4
|
+
import { X, ZoomIn, ZoomOut, RotateCcw } from "lucide-react";
|
|
5
|
+
import { useEffect, useState, useCallback, useRef } from "react";
|
|
6
|
+
import { createPortal } from "react-dom";
|
|
7
|
+
|
|
8
|
+
interface ImageZoomOverlayProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
altText?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ImageZoomOverlay({ isOpen, onClose, imageUrl, altText }: ImageZoomOverlayProps) {
|
|
16
|
+
const [mounted, setMounted] = useState(false);
|
|
17
|
+
const [scale, setScale] = useState(1);
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
// Motion values for panning
|
|
21
|
+
const x = useMotionValue(0);
|
|
22
|
+
const y = useMotionValue(0);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// Reset zoom and pan when opening/closing
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isOpen) {
|
|
31
|
+
setScale(1);
|
|
32
|
+
x.set(0);
|
|
33
|
+
y.set(0);
|
|
34
|
+
}
|
|
35
|
+
}, [isOpen, x, y]);
|
|
36
|
+
|
|
37
|
+
// Body scroll lock
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (isOpen) {
|
|
40
|
+
document.body.style.overflow = 'hidden';
|
|
41
|
+
return () => {
|
|
42
|
+
document.body.style.overflow = '';
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}, [isOpen]);
|
|
46
|
+
|
|
47
|
+
const handleWheel = useCallback((e: React.WheelEvent) => {
|
|
48
|
+
if (e.ctrlKey || e.metaKey) { // OS-level pinch gesture often maps to ctrl+wheel
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
const delta = -e.deltaY * 0.01;
|
|
51
|
+
setScale(prev => Math.min(Math.max(1, prev + delta), 5));
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const reset = () => {
|
|
56
|
+
setScale(1);
|
|
57
|
+
x.set(0);
|
|
58
|
+
y.set(0);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!mounted) return null;
|
|
62
|
+
|
|
63
|
+
const content = (
|
|
64
|
+
<AnimatePresence>
|
|
65
|
+
{isOpen && (
|
|
66
|
+
<div
|
|
67
|
+
className="fixed inset-0 z-[999999] flex items-center justify-center overflow-hidden touch-none"
|
|
68
|
+
onWheel={handleWheel}
|
|
69
|
+
>
|
|
70
|
+
{/* Backdrop */}
|
|
71
|
+
<motion.div
|
|
72
|
+
initial={{ opacity: 0 }}
|
|
73
|
+
animate={{ opacity: 1 }}
|
|
74
|
+
exit={{ opacity: 0 }}
|
|
75
|
+
onClick={onClose}
|
|
76
|
+
className="absolute inset-0 bg-black/95 backdrop-blur-xl cursor-zoom-out"
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{/* Controls HUD */}
|
|
80
|
+
<motion.div
|
|
81
|
+
initial={{ opacity: 0, y: 20 }}
|
|
82
|
+
animate={{ opacity: 1, y: 0 }}
|
|
83
|
+
exit={{ opacity: 0, y: 20 }}
|
|
84
|
+
className="absolute bottom-10 left-1/2 -translate-x-1/2 z-[10] flex items-center gap-4 px-6 py-3 rounded-2xl bg-white/5 border border-white/10 backdrop-blur-md shadow-2xl"
|
|
85
|
+
>
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => setScale(s => Math.max(1, s - 0.5))}
|
|
88
|
+
className="p-2 rounded-xl hover:bg-white/10 text-white/60 hover:text-white transition-all"
|
|
89
|
+
title="Zoom Out"
|
|
90
|
+
>
|
|
91
|
+
<ZoomOut className="w-5 h-5" />
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
<div className="w-px h-4 bg-white/10 mx-2" />
|
|
95
|
+
|
|
96
|
+
<span className="text-xs font-mono text-white/40 min-w-[3rem] text-center">
|
|
97
|
+
{Math.round(scale * 100)}%
|
|
98
|
+
</span>
|
|
99
|
+
|
|
100
|
+
<div className="w-px h-4 bg-white/10 mx-2" />
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => setScale(s => Math.min(5, s + 0.5))}
|
|
104
|
+
className="p-2 rounded-xl hover:bg-white/10 text-white/60 hover:text-white transition-all"
|
|
105
|
+
title="Zoom In"
|
|
106
|
+
>
|
|
107
|
+
<ZoomIn className="w-5 h-5" />
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<button
|
|
111
|
+
onClick={reset}
|
|
112
|
+
className="p-2 rounded-xl hover:bg-white/10 text-white/60 hover:text-white transition-all ml-2"
|
|
113
|
+
title="Reset"
|
|
114
|
+
>
|
|
115
|
+
<RotateCcw className="w-4 h-4" />
|
|
116
|
+
</button>
|
|
117
|
+
</motion.div>
|
|
118
|
+
|
|
119
|
+
{/* Close Button */}
|
|
120
|
+
<motion.button
|
|
121
|
+
initial={{ opacity: 0, scale: 0.5 }}
|
|
122
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
123
|
+
exit={{ opacity: 0, scale: 0.5 }}
|
|
124
|
+
onClick={onClose}
|
|
125
|
+
className="absolute top-8 right-8 z-[10] p-4 rounded-full bg-white/10 hover:bg-white/20 text-white/80 transition-all border border-white/10"
|
|
126
|
+
>
|
|
127
|
+
<X className="w-6 h-6" />
|
|
128
|
+
</motion.button>
|
|
129
|
+
|
|
130
|
+
{/* Hint text */}
|
|
131
|
+
<motion.p
|
|
132
|
+
initial={{ opacity: 0 }}
|
|
133
|
+
animate={{ opacity: 1 }}
|
|
134
|
+
className="absolute top-8 left-1/2 -translate-x-1/2 text-[10px] uppercase tracking-[0.3em] text-white/20 font-black"
|
|
135
|
+
>
|
|
136
|
+
Pinch_or_Scroll_to_Explore
|
|
137
|
+
</motion.p>
|
|
138
|
+
|
|
139
|
+
{/* Image Container */}
|
|
140
|
+
<motion.div
|
|
141
|
+
ref={containerRef}
|
|
142
|
+
className="relative w-full h-full flex items-center justify-center p-4 md:p-20"
|
|
143
|
+
style={{ perspective: 1000 }}
|
|
144
|
+
>
|
|
145
|
+
<motion.img
|
|
146
|
+
src={imageUrl}
|
|
147
|
+
alt={altText || "Zoomed project image"}
|
|
148
|
+
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
|
149
|
+
animate={{
|
|
150
|
+
opacity: 1,
|
|
151
|
+
scale: scale,
|
|
152
|
+
x: x.get(),
|
|
153
|
+
y: y.get(),
|
|
154
|
+
rotateX: 0,
|
|
155
|
+
transition: {
|
|
156
|
+
opacity: { duration: 0.4 },
|
|
157
|
+
scale: { type: "spring", stiffness: 300, damping: 30 },
|
|
158
|
+
y: { type: "spring", stiffness: 300, damping: 30 }
|
|
159
|
+
}
|
|
160
|
+
}}
|
|
161
|
+
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
|
162
|
+
drag={scale > 1}
|
|
163
|
+
dragConstraints={containerRef}
|
|
164
|
+
onDrag={(e, info) => {
|
|
165
|
+
x.set(x.get() + info.delta.x);
|
|
166
|
+
y.set(y.get() + info.delta.y);
|
|
167
|
+
}}
|
|
168
|
+
className={`max-w-full max-h-full object-contain shadow-[0_0_100px_rgba(0,0,0,0.5)] rounded-lg md:rounded-2xl transition-shadow duration-500 ${scale > 1 ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`}
|
|
169
|
+
draggable={false}
|
|
170
|
+
/>
|
|
171
|
+
</motion.div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</AnimatePresence>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return createPortal(content, document.body);
|
|
178
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp"
|
|
5
|
+
import { Dot } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
const InputOTP = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof OTPInput>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof OTPInput>
|
|
12
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
13
|
+
<OTPInput
|
|
14
|
+
ref={ref}
|
|
15
|
+
containerClassName={cn(
|
|
16
|
+
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
|
17
|
+
containerClassName
|
|
18
|
+
)}
|
|
19
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
))
|
|
23
|
+
InputOTP.displayName = "InputOTP"
|
|
24
|
+
|
|
25
|
+
const InputOTPGroup = React.forwardRef<
|
|
26
|
+
React.ElementRef<"div">,
|
|
27
|
+
React.ComponentPropsWithoutRef<"div">
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
|
30
|
+
))
|
|
31
|
+
InputOTPGroup.displayName = "InputOTPGroup"
|
|
32
|
+
|
|
33
|
+
const InputOTPSlot = React.forwardRef<
|
|
34
|
+
React.ElementRef<"div">,
|
|
35
|
+
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
|
36
|
+
>(({ index, className, ...props }, ref) => {
|
|
37
|
+
const inputOTPContext = React.useContext(OTPInputContext)
|
|
38
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
|
45
|
+
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{char}
|
|
51
|
+
{hasFakeCaret && (
|
|
52
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
53
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
InputOTPSlot.displayName = "InputOTPSlot"
|
|
60
|
+
|
|
61
|
+
const InputOTPSeparator = React.forwardRef<
|
|
62
|
+
React.ElementRef<"div">,
|
|
63
|
+
React.ComponentPropsWithoutRef<"div">
|
|
64
|
+
>(({ ...props }, ref) => (
|
|
65
|
+
<div ref={ref} role="separator" {...props}>
|
|
66
|
+
<Dot />
|
|
67
|
+
</div>
|
|
68
|
+
))
|
|
69
|
+
InputOTPSeparator.displayName = "InputOTPSeparator"
|
|
70
|
+
|
|
71
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|