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,152 @@
|
|
|
1
|
+
|
|
2
|
+
"use client"
|
|
3
|
+
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
import { useRouter } from "next/navigation"
|
|
6
|
+
import {
|
|
7
|
+
CommandMenu,
|
|
8
|
+
CommandMenuContent,
|
|
9
|
+
CommandMenuInput,
|
|
10
|
+
CommandMenuList,
|
|
11
|
+
CommandMenuGroup,
|
|
12
|
+
CommandMenuItem,
|
|
13
|
+
CommandMenuSeparator,
|
|
14
|
+
useCommandMenuShortcut,
|
|
15
|
+
} from "@/components/ui/command-menu"
|
|
16
|
+
import {
|
|
17
|
+
Home,
|
|
18
|
+
User,
|
|
19
|
+
Briefcase,
|
|
20
|
+
Github,
|
|
21
|
+
Twitter,
|
|
22
|
+
Linkedin,
|
|
23
|
+
Mail,
|
|
24
|
+
FileText,
|
|
25
|
+
Copy,
|
|
26
|
+
ArrowRight
|
|
27
|
+
} from "lucide-react"
|
|
28
|
+
|
|
29
|
+
export function CommandPalette() {
|
|
30
|
+
const [open, setOpen] = React.useState(false)
|
|
31
|
+
const router = useRouter()
|
|
32
|
+
|
|
33
|
+
useCommandMenuShortcut(() => setOpen(true))
|
|
34
|
+
|
|
35
|
+
const runCommand = React.useCallback((command: () => void) => {
|
|
36
|
+
setOpen(false)
|
|
37
|
+
command()
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<CommandMenu open={open} onOpenChange={setOpen}>
|
|
42
|
+
{/* Trigger is handled globally by shortcut, so no visual trigger needed here */}
|
|
43
|
+
|
|
44
|
+
<CommandMenuContent>
|
|
45
|
+
<CommandMenuInput placeholder="Type a command or search..." />
|
|
46
|
+
<CommandMenuList>
|
|
47
|
+
<CommandMenuGroup heading="Explore the Verse">
|
|
48
|
+
<CommandMenuItem
|
|
49
|
+
icon={<Home className="text-blue-400" />}
|
|
50
|
+
index={0}
|
|
51
|
+
keywords={["home", "hq", "index", "return"]}
|
|
52
|
+
onSelect={() => runCommand(() => router.push("/"))}
|
|
53
|
+
>
|
|
54
|
+
<span className="font-medium text-white/90">Return to HQ</span>
|
|
55
|
+
<span className="ml-2 text-xs text-white/40 hidden sm:inline-block">Home Page</span>
|
|
56
|
+
</CommandMenuItem>
|
|
57
|
+
<CommandMenuItem
|
|
58
|
+
icon={<User className="text-purple-400" />}
|
|
59
|
+
index={1}
|
|
60
|
+
keywords={["about", "bio", "lore", "who", "story"]}
|
|
61
|
+
onSelect={() => runCommand(() => router.push("/about"))}
|
|
62
|
+
>
|
|
63
|
+
<span className="font-medium text-white/90">Who is Abhishek?</span>
|
|
64
|
+
<span className="ml-2 text-xs text-white/40 hidden sm:inline-block">The lore behind the dev</span>
|
|
65
|
+
</CommandMenuItem>
|
|
66
|
+
<CommandMenuItem
|
|
67
|
+
icon={<Briefcase className="text-yellow-400" />}
|
|
68
|
+
index={2}
|
|
69
|
+
keywords={["works", "projects", "portfolio", "archives", "case studies"]}
|
|
70
|
+
onSelect={() => runCommand(() => router.push("/works"))}
|
|
71
|
+
>
|
|
72
|
+
<span className="font-medium text-white/90">Inspect the Archives</span>
|
|
73
|
+
<span className="ml-2 text-xs text-white/40 hidden sm:inline-block">My finest works</span>
|
|
74
|
+
</CommandMenuItem>
|
|
75
|
+
<CommandMenuItem
|
|
76
|
+
icon={<Github className="text-green-400" />}
|
|
77
|
+
index={3}
|
|
78
|
+
keywords={["github", "code", "source", "repo", "git"]}
|
|
79
|
+
onSelect={() => runCommand(() => router.push("/github/AbhishekS04"))}
|
|
80
|
+
>
|
|
81
|
+
<span className="font-medium text-white/90">Analyze Source Code</span>
|
|
82
|
+
<span className="ml-2 text-xs text-white/40 hidden sm:inline-block">View GitHub without leaving</span>
|
|
83
|
+
</CommandMenuItem>
|
|
84
|
+
</CommandMenuGroup>
|
|
85
|
+
|
|
86
|
+
<CommandMenuSeparator />
|
|
87
|
+
|
|
88
|
+
<CommandMenuGroup heading="Connect">
|
|
89
|
+
<CommandMenuItem
|
|
90
|
+
icon={<Github />}
|
|
91
|
+
index={4}
|
|
92
|
+
keywords={["github", "profile", "social"]}
|
|
93
|
+
onSelect={() => runCommand(() => window.open("https://github.com/AbhishekS04", "_blank"))}
|
|
94
|
+
>
|
|
95
|
+
GitHub Profile
|
|
96
|
+
</CommandMenuItem>
|
|
97
|
+
<CommandMenuItem
|
|
98
|
+
icon={<Twitter />}
|
|
99
|
+
index={5}
|
|
100
|
+
keywords={["twitter", "x", "social", "tweet"]}
|
|
101
|
+
onSelect={() => runCommand(() => window.open("https://twitter.com/AbhishekS04", "_blank"))}
|
|
102
|
+
>
|
|
103
|
+
Twitter / X
|
|
104
|
+
</CommandMenuItem>
|
|
105
|
+
<CommandMenuItem
|
|
106
|
+
icon={<Linkedin />}
|
|
107
|
+
index={6}
|
|
108
|
+
keywords={["linkedin", "network", "social", "career"]}
|
|
109
|
+
onSelect={() => runCommand(() => window.open("https://linkedin.com/in/AbhishekS04", "_blank"))}
|
|
110
|
+
>
|
|
111
|
+
LinkedIn Network
|
|
112
|
+
</CommandMenuItem>
|
|
113
|
+
</CommandMenuGroup>
|
|
114
|
+
|
|
115
|
+
<CommandMenuSeparator />
|
|
116
|
+
|
|
117
|
+
<CommandMenuGroup heading="Protocol">
|
|
118
|
+
<CommandMenuItem
|
|
119
|
+
icon={<Copy />}
|
|
120
|
+
index={7}
|
|
121
|
+
shortcut="cmd+c"
|
|
122
|
+
keywords={["copy", "email", "address", "contact"]}
|
|
123
|
+
onSelect={() => runCommand(() => {
|
|
124
|
+
navigator.clipboard.writeText("your.email@example.com")
|
|
125
|
+
})}
|
|
126
|
+
>
|
|
127
|
+
Copy Coordinates (Email)
|
|
128
|
+
</CommandMenuItem>
|
|
129
|
+
<CommandMenuItem
|
|
130
|
+
icon={<FileText />}
|
|
131
|
+
index={8}
|
|
132
|
+
shortcut="cmd+r"
|
|
133
|
+
keywords={["resume", "cv", "pdf", "dossier", "download"]}
|
|
134
|
+
onSelect={() => runCommand(() => window.open("/resume.pdf", "_blank"))}
|
|
135
|
+
>
|
|
136
|
+
Download Dossier (Resume)
|
|
137
|
+
</CommandMenuItem>
|
|
138
|
+
<CommandMenuItem
|
|
139
|
+
icon={<Mail />}
|
|
140
|
+
index={9}
|
|
141
|
+
keywords={["send", "mail", "contact", "message", "write"]}
|
|
142
|
+
onSelect={() => runCommand(() => window.location.href = "mailto:your.email@example.com")}
|
|
143
|
+
>
|
|
144
|
+
Establish Comms (Mailto)
|
|
145
|
+
</CommandMenuItem>
|
|
146
|
+
</CommandMenuGroup>
|
|
147
|
+
</CommandMenuList>
|
|
148
|
+
</CommandMenuContent>
|
|
149
|
+
</CommandMenu>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
|
|
6
|
+
export const ConsciousnessMode = () => {
|
|
7
|
+
const [isActive, setIsActive] = useState(false);
|
|
8
|
+
const [progress, setProgress] = useState(0);
|
|
9
|
+
const [isLocked, setIsLocked] = useState(true); // Default locked until checked
|
|
10
|
+
const progressRef = useRef(0);
|
|
11
|
+
const lastScrollY = useRef(0);
|
|
12
|
+
const lastTime = useRef(0);
|
|
13
|
+
const inactivityTimer = useRef<NodeJS.Timeout | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Check persistence
|
|
17
|
+
const witnessed = localStorage.getItem("consciousness_witnessed");
|
|
18
|
+
if (!witnessed) setIsLocked(false);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const handleScroll = (e: Event) => {
|
|
23
|
+
if (isActive) return;
|
|
24
|
+
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const currentScrollY = window.scrollY;
|
|
27
|
+
const deltaY = Math.abs(currentScrollY - lastScrollY.current);
|
|
28
|
+
const deltaTime = now - lastTime.current;
|
|
29
|
+
|
|
30
|
+
// Update refs
|
|
31
|
+
lastScrollY.current = currentScrollY;
|
|
32
|
+
lastTime.current = now;
|
|
33
|
+
|
|
34
|
+
// LOGIC:
|
|
35
|
+
// 1. Shift Key must be held (we check via a separate listener or just window.event if possible, but React safer to track key state)
|
|
36
|
+
// Actually, we can check keyboard state via a ref tracker.
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// We need a key tracker
|
|
40
|
+
// But simpler: We can check MouseEvent modifiers? No, scroll is an Event or WheelEvent.
|
|
41
|
+
// WheelEvent has shiftKey. But 'scroll' event does not.
|
|
42
|
+
// Let's listen to 'wheel' for the trigger?
|
|
43
|
+
// - "scroll" event fires AFTER layout change.
|
|
44
|
+
// - "wheel" event fires ON input.
|
|
45
|
+
// User said "Scroll slowly". Wheel is better for detecting "Intent".
|
|
46
|
+
|
|
47
|
+
}, [isActive]);
|
|
48
|
+
|
|
49
|
+
// Better Logic:
|
|
50
|
+
// Track Shift Key State
|
|
51
|
+
const [isShiftPressed, setIsShiftPressed] = useState(false);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isLocked) return;
|
|
54
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
55
|
+
if (e.key === "Shift") setIsShiftPressed(true);
|
|
56
|
+
};
|
|
57
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
58
|
+
if (e.key === "Shift") {
|
|
59
|
+
setIsShiftPressed(false);
|
|
60
|
+
if (!isActive) {
|
|
61
|
+
setProgress(0); // Reset if let go before completion
|
|
62
|
+
progressRef.current = 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
67
|
+
window.addEventListener("keyup", handleKeyUp);
|
|
68
|
+
return () => {
|
|
69
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
70
|
+
window.removeEventListener("keyup", handleKeyUp);
|
|
71
|
+
};
|
|
72
|
+
}, [isActive, isLocked]);
|
|
73
|
+
|
|
74
|
+
// Track Scroll
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (isLocked || !isShiftPressed || isActive) return;
|
|
77
|
+
|
|
78
|
+
let animationFrame: number;
|
|
79
|
+
|
|
80
|
+
const checkScroll = () => {
|
|
81
|
+
// We need to detect "Active Scrolling" but "Slow".
|
|
82
|
+
// Since scroll events are discrete, let's just decay the progress if NO scroll happens.
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleWheel = (e: WheelEvent) => {
|
|
86
|
+
if (isLocked || !isShiftPressed || isActive) return;
|
|
87
|
+
|
|
88
|
+
// Speed Check
|
|
89
|
+
// Typical fast scroll is > 50-100 delta.
|
|
90
|
+
// Slow scroll is < 20.
|
|
91
|
+
const speed = Math.abs(e.deltaY);
|
|
92
|
+
|
|
93
|
+
if (speed > 0 && speed < 30) {
|
|
94
|
+
// Good speed. Increment.
|
|
95
|
+
// Target: 7 seconds.
|
|
96
|
+
// Assuming ~60 wheel events per second for continuous smooth scrolling (trackpad), or ~10 for ratcheted mouse.
|
|
97
|
+
// Let's aim safely for a mix. 0.4 per event is roughly 250 events.
|
|
98
|
+
// If 60hz -> 4 seconds. If 30hz -> 8 seconds.
|
|
99
|
+
// Let's try 0.25 to be safe for 7s on smooth trackpads.
|
|
100
|
+
progressRef.current += 0.25;
|
|
101
|
+
|
|
102
|
+
if (progressRef.current > 100) {
|
|
103
|
+
progressRef.current = 100;
|
|
104
|
+
setIsActive(true);
|
|
105
|
+
localStorage.setItem("consciousness_witnessed", "true");
|
|
106
|
+
}
|
|
107
|
+
setProgress(progressRef.current);
|
|
108
|
+
} else if (speed > 50) {
|
|
109
|
+
// Too fast! Reset punishment.
|
|
110
|
+
progressRef.current = Math.max(0, progressRef.current - 5);
|
|
111
|
+
setProgress(progressRef.current);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Reset inactivity timer
|
|
115
|
+
if (inactivityTimer.current) clearTimeout(inactivityTimer.current);
|
|
116
|
+
inactivityTimer.current = setTimeout(() => {
|
|
117
|
+
// Decay if stopped
|
|
118
|
+
const decay = setInterval(() => {
|
|
119
|
+
progressRef.current -= 2;
|
|
120
|
+
if (progressRef.current <= 0) {
|
|
121
|
+
progressRef.current = 0;
|
|
122
|
+
clearInterval(decay);
|
|
123
|
+
}
|
|
124
|
+
setProgress(progressRef.current);
|
|
125
|
+
}, 50);
|
|
126
|
+
}, 500);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
window.addEventListener("wheel", handleWheel);
|
|
130
|
+
return () => window.removeEventListener("wheel", handleWheel);
|
|
131
|
+
}, [isShiftPressed, isActive, isLocked]);
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// Auto-dismiss after activation
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (isActive) {
|
|
137
|
+
const timer = setTimeout(() => {
|
|
138
|
+
setIsActive(false);
|
|
139
|
+
setProgress(0);
|
|
140
|
+
progressRef.current = 0;
|
|
141
|
+
}, 4500); // Wait for text to fully play out
|
|
142
|
+
return () => clearTimeout(timer);
|
|
143
|
+
}
|
|
144
|
+
}, [isActive]);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<AnimatePresence>
|
|
148
|
+
{/* PROGRESS FEEDBACK (Subtle) */}
|
|
149
|
+
{progress > 5 && !isActive && (
|
|
150
|
+
<motion.div
|
|
151
|
+
className="fixed bottom-0 left-0 h-1 bg-white/20 z-[9999]"
|
|
152
|
+
style={{ width: `${progress}%` }}
|
|
153
|
+
initial={{ opacity: 0 }}
|
|
154
|
+
animate={{ opacity: 1 }}
|
|
155
|
+
exit={{ opacity: 0 }}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{isActive && (
|
|
160
|
+
<motion.div
|
|
161
|
+
initial={{ opacity: 0 }}
|
|
162
|
+
animate={{ opacity: 1 }}
|
|
163
|
+
exit={{ opacity: 0 }}
|
|
164
|
+
transition={{ duration: 1 }}
|
|
165
|
+
className="fixed inset-0 z-[1000] pointer-events-none flex items-center justify-center"
|
|
166
|
+
>
|
|
167
|
+
{/* BLUR OVERLAY */}
|
|
168
|
+
<motion.div
|
|
169
|
+
initial={{ backdropFilter: "blur(0px)", backgroundColor: "rgba(0,0,0,0)" }}
|
|
170
|
+
animate={{ backdropFilter: "blur(12px)", backgroundColor: "rgba(0,0,0,0.4)" }}
|
|
171
|
+
exit={{ backdropFilter: "blur(0px)", backgroundColor: "rgba(0,0,0,0)" }}
|
|
172
|
+
transition={{ duration: 1.5, ease: "easeOut" }}
|
|
173
|
+
className="absolute inset-0"
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
{/* TEXT */}
|
|
177
|
+
<motion.div
|
|
178
|
+
initial={{ opacity: 0, scale: 0.95, filter: "blur(10px)" }}
|
|
179
|
+
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
|
180
|
+
exit={{ opacity: 0, scale: 1.05, filter: "blur(5px)" }}
|
|
181
|
+
transition={{ duration: 1.5, ease: "easeOut" }}
|
|
182
|
+
className="relative z-10 text-center px-4"
|
|
183
|
+
>
|
|
184
|
+
<h2 className="text-3xl md:text-5xl font-light tracking-widest text-[#e2e2e2] font-serif italic mb-4">
|
|
185
|
+
You’re paying attention.
|
|
186
|
+
</h2>
|
|
187
|
+
<motion.p
|
|
188
|
+
initial={{ opacity: 0 }}
|
|
189
|
+
animate={{ opacity: 1 }}
|
|
190
|
+
transition={{ delay: 1.5, duration: 1 }}
|
|
191
|
+
className="text-sm uppercase tracking-[0.3em] text-white/50"
|
|
192
|
+
>
|
|
193
|
+
I like that.
|
|
194
|
+
</motion.p>
|
|
195
|
+
</motion.div>
|
|
196
|
+
</motion.div>
|
|
197
|
+
)}
|
|
198
|
+
</AnimatePresence>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
interface CopyCodeProps {
|
|
6
|
+
code: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CopyCode({ code }: CopyCodeProps) {
|
|
10
|
+
const [copied, setCopied] = useState(false);
|
|
11
|
+
const [showConfirmation, setShowConfirmation] = useState(false);
|
|
12
|
+
const [progress, setProgress] = useState(0);
|
|
13
|
+
const duration = 2000; // Reduced to 2s for email copy (4s is too long)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (copied) {
|
|
17
|
+
// Delay showing confirmation to allow blur-out animation
|
|
18
|
+
const showTimer = setTimeout(() => {
|
|
19
|
+
setShowConfirmation(true);
|
|
20
|
+
}, 400);
|
|
21
|
+
|
|
22
|
+
setProgress(0);
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
|
|
25
|
+
const interval = setInterval(() => {
|
|
26
|
+
const elapsed = Date.now() - startTime;
|
|
27
|
+
const newProgress = Math.min((elapsed / duration) * 100, 100);
|
|
28
|
+
setProgress(newProgress);
|
|
29
|
+
|
|
30
|
+
if (elapsed >= duration) {
|
|
31
|
+
clearInterval(interval);
|
|
32
|
+
setShowConfirmation(false);
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
setCopied(false);
|
|
35
|
+
setProgress(0);
|
|
36
|
+
}, 400);
|
|
37
|
+
}
|
|
38
|
+
}, 16);
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
clearTimeout(showTimer);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}, [copied]);
|
|
46
|
+
|
|
47
|
+
const handleCopy = async () => {
|
|
48
|
+
try {
|
|
49
|
+
await navigator.clipboard.writeText(code);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// Fallback
|
|
52
|
+
const textArea = document.createElement('textarea');
|
|
53
|
+
textArea.value = code;
|
|
54
|
+
document.body.appendChild(textArea);
|
|
55
|
+
textArea.select();
|
|
56
|
+
document.execCommand('copy');
|
|
57
|
+
document.body.removeChild(textArea);
|
|
58
|
+
}
|
|
59
|
+
setCopied(true);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="relative overflow-hidden flex items-center justify-between bg-[#111] border border-white/10 rounded-full px-8 py-3 w-fit min-w-[300px] h-16">
|
|
64
|
+
{/* Progress background */}
|
|
65
|
+
<div
|
|
66
|
+
className="absolute left-0 top-0 bottom-0 bg-white/10"
|
|
67
|
+
style={{
|
|
68
|
+
width: `${progress}%`,
|
|
69
|
+
opacity: copied ? 1 : 0,
|
|
70
|
+
transition: 'opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
{/* Original content - code and button */}
|
|
75
|
+
<div
|
|
76
|
+
className="absolute inset-0 flex items-center justify-between px-8"
|
|
77
|
+
style={{
|
|
78
|
+
opacity: copied ? 0 : 1,
|
|
79
|
+
filter: copied ? 'blur(12px)' : 'blur(0px)',
|
|
80
|
+
transform: copied ? 'scale(0.92)' : 'scale(1)',
|
|
81
|
+
transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
82
|
+
pointerEvents: copied ? 'none' : 'auto',
|
|
83
|
+
zIndex: copied ? 0 : 20,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<span className="text-xl font-medium tracking-wide text-white/80 select-all truncate max-w-[200px]">
|
|
87
|
+
{code}
|
|
88
|
+
</span>
|
|
89
|
+
<button
|
|
90
|
+
onClick={handleCopy}
|
|
91
|
+
className="ml-4 bg-white/10 hover:bg-white/20 text-white/90 text-sm font-medium px-4 py-2 rounded-full transition-all duration-300 active:scale-95 cursor-pointer select-none"
|
|
92
|
+
>
|
|
93
|
+
Copy
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Confirmation content - Code Copied! */}
|
|
98
|
+
<div
|
|
99
|
+
className="absolute inset-0 flex items-center justify-center gap-3"
|
|
100
|
+
style={{
|
|
101
|
+
opacity: showConfirmation ? 1 : 0,
|
|
102
|
+
filter: showConfirmation ? 'blur(0px)' : 'blur(12px)',
|
|
103
|
+
transform: showConfirmation ? 'scale(1)' : 'scale(1.08)',
|
|
104
|
+
transition: 'all 0.8s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
105
|
+
pointerEvents: 'none',
|
|
106
|
+
zIndex: 10,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<div className="w-6 h-6 bg-emerald-500 rounded-full flex items-center justify-center">
|
|
110
|
+
<svg
|
|
111
|
+
className="w-3.5 h-3.5 text-black"
|
|
112
|
+
fill="none"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
viewBox="0 0 24 24"
|
|
115
|
+
>
|
|
116
|
+
<path
|
|
117
|
+
strokeLinecap="round"
|
|
118
|
+
strokeLinejoin="round"
|
|
119
|
+
strokeWidth={3}
|
|
120
|
+
d="M5 13l4 4L19 7"
|
|
121
|
+
style={{
|
|
122
|
+
strokeDasharray: 24,
|
|
123
|
+
strokeDashoffset: showConfirmation ? 0 : 24,
|
|
124
|
+
transition: 'stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.3s',
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</svg>
|
|
128
|
+
</div>
|
|
129
|
+
<span className="text-lg font-medium text-white">
|
|
130
|
+
Email Copied!
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Sparkles } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface DisplayCardProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
date?: string;
|
|
12
|
+
iconClassName?: string;
|
|
13
|
+
titleClassName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function DisplayCard({
|
|
17
|
+
className,
|
|
18
|
+
icon = <Sparkles className="size-4 text-blue-300" />,
|
|
19
|
+
title = "Featured",
|
|
20
|
+
description = "Discover amazing content",
|
|
21
|
+
date = "Just now",
|
|
22
|
+
iconClassName = "text-blue-500",
|
|
23
|
+
titleClassName = "text-blue-500",
|
|
24
|
+
}: DisplayCardProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
"relative flex h-36 w-full max-w-[22rem] -skew-y-[8deg] select-none flex-col justify-between rounded-xl border-2 bg-muted/70 backdrop-blur-sm px-4 py-3 transition-all duration-700 after:absolute after:-right-1 after:top-[-5%] after:h-[110%] after:w-[20rem] after:bg-gradient-to-l after:from-background after:to-transparent after:content-[''] hover:border-white/20 hover:bg-muted [&>*]:flex [&>*]:items-center [&>*]:gap-2",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<div>
|
|
33
|
+
<span className="relative inline-block rounded-full bg-blue-800 p-1">
|
|
34
|
+
{icon}
|
|
35
|
+
</span>
|
|
36
|
+
<p className={cn("text-lg font-medium", titleClassName)}>{title}</p>
|
|
37
|
+
</div>
|
|
38
|
+
<p className="whitespace-nowrap text-lg">{description}</p>
|
|
39
|
+
<p className="text-muted-foreground">{date}</p>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface DisplayCardsProps {
|
|
45
|
+
cards?: DisplayCardProps[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function DisplayCards({ cards }: DisplayCardsProps) {
|
|
49
|
+
const defaultCards = [
|
|
50
|
+
{
|
|
51
|
+
className: "[grid-area:stack] hover:-translate-y-10 before:absolute before:w-[100%] before:outline-1 before:rounded-xl before:outline-border before:h-[100%] before:content-[''] before:bg-blend-overlay before:bg-background/50 grayscale-[100%] hover:before:opacity-0 before:transition-opacity before:duration:700 hover:grayscale-0 before:left-0 before:top-0",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
className: "[grid-area:stack] translate-x-16 translate-y-10 hover:-translate-y-1 before:absolute before:w-[100%] before:outline-1 before:rounded-xl before:outline-border before:h-[100%] before:content-[''] before:bg-blend-overlay before:bg-background/50 grayscale-[100%] hover:before:opacity-0 before:transition-opacity before:duration:700 hover:grayscale-0 before:left-0 before:top-0",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
className: "[grid-area:stack] translate-x-32 translate-y-20 hover:translate-y-10",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const displayCards = cards || defaultCards;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="grid [grid-template-areas:'stack'] place-items-center opacity-100 animate-in fade-in-0 duration-700">
|
|
65
|
+
{displayCards.map((cardProps, index) => (
|
|
66
|
+
<DisplayCard key={index} {...cardProps} />
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { createMap } from "svg-dotted-map"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
interface Marker {
|
|
7
|
+
lat: number
|
|
8
|
+
lng: number
|
|
9
|
+
size?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DottedMapProps extends React.SVGProps<SVGSVGElement> {
|
|
13
|
+
width?: number
|
|
14
|
+
height?: number
|
|
15
|
+
mapSamples?: number
|
|
16
|
+
markers?: Marker[]
|
|
17
|
+
dotColor?: string
|
|
18
|
+
markerColor?: string
|
|
19
|
+
dotRadius?: number
|
|
20
|
+
stagger?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DottedMap({
|
|
24
|
+
width = 150,
|
|
25
|
+
height = 75,
|
|
26
|
+
mapSamples = 5000,
|
|
27
|
+
markers = [],
|
|
28
|
+
markerColor = "#FF6900",
|
|
29
|
+
dotRadius = 0.2,
|
|
30
|
+
stagger = true,
|
|
31
|
+
className,
|
|
32
|
+
style,
|
|
33
|
+
}: DottedMapProps) {
|
|
34
|
+
const { points, addMarkers } = createMap({
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
mapSamples,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const processedMarkers = addMarkers(markers)
|
|
41
|
+
|
|
42
|
+
// Compute stagger helpers in a single, simple pass
|
|
43
|
+
const { xStep, yToRowIndex } = React.useMemo(() => {
|
|
44
|
+
const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x)
|
|
45
|
+
const rowMap = new Map<number, number>()
|
|
46
|
+
let step = 0
|
|
47
|
+
let prevY = Number.NaN
|
|
48
|
+
let prevXInRow = Number.NaN
|
|
49
|
+
|
|
50
|
+
for (const p of sorted) {
|
|
51
|
+
if (p.y !== prevY) {
|
|
52
|
+
// new row
|
|
53
|
+
prevY = p.y
|
|
54
|
+
prevXInRow = Number.NaN
|
|
55
|
+
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isNaN(prevXInRow)) {
|
|
58
|
+
const delta = p.x - prevXInRow
|
|
59
|
+
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
|
|
60
|
+
}
|
|
61
|
+
prevXInRow = p.x
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { xStep: step || 1, yToRowIndex: rowMap }
|
|
65
|
+
}, [points])
|
|
66
|
+
|
|
67
|
+
const [isEggActive, setIsEggActive] = React.useState(false);
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (typeof window !== "undefined") {
|
|
71
|
+
const purged = localStorage.getItem("mapEgg");
|
|
72
|
+
if (purged !== "purged") setIsEggActive(true);
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const handleEggClick = (e: React.MouseEvent) => {
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
setIsEggActive(false);
|
|
80
|
+
localStorage.setItem("mapEgg", "purged");
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<svg
|
|
85
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
86
|
+
className={cn("text-gray-500 dark:text-gray-500", className)}
|
|
87
|
+
style={{ width: "100%", height: "100%", ...style }}
|
|
88
|
+
>
|
|
89
|
+
{points.map((point, index) => {
|
|
90
|
+
const rowIndex = yToRowIndex.get(point.y) ?? 0
|
|
91
|
+
const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0
|
|
92
|
+
return (
|
|
93
|
+
<circle
|
|
94
|
+
cx={point.x + offsetX}
|
|
95
|
+
cy={point.y}
|
|
96
|
+
r={dotRadius}
|
|
97
|
+
fill="currentColor"
|
|
98
|
+
key={`${point.x}-${point.y}-${index}`}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
})}
|
|
102
|
+
{processedMarkers.map((marker, index) => {
|
|
103
|
+
const rowIndex = yToRowIndex.get(marker.y) ?? 0
|
|
104
|
+
const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0
|
|
105
|
+
return (
|
|
106
|
+
<g key={`${marker.x}-${marker.y}-${index}`}>
|
|
107
|
+
<circle
|
|
108
|
+
cx={marker.x + offsetX}
|
|
109
|
+
cy={marker.y}
|
|
110
|
+
r={marker.size ?? dotRadius}
|
|
111
|
+
fill={markerColor}
|
|
112
|
+
/>
|
|
113
|
+
{isEggActive && (
|
|
114
|
+
<circle
|
|
115
|
+
cx={marker.x + offsetX}
|
|
116
|
+
cy={marker.y}
|
|
117
|
+
r={(marker.size ?? dotRadius) * 4} // ~4x radius for comfortable click area
|
|
118
|
+
fill="transparent"
|
|
119
|
+
className="cursor-default pointer-events-auto"
|
|
120
|
+
onClick={handleEggClick}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
</g>
|
|
124
|
+
)
|
|
125
|
+
})}
|
|
126
|
+
</svg>
|
|
127
|
+
)
|
|
128
|
+
}
|