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,148 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { SocialStories } from "@/components/ui/social-stories"
|
|
5
|
+
import { bolivia } from "@/lib/fonts"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { useState } from "react"
|
|
8
|
+
import { MobileMenu } from "./mobile-menu"
|
|
9
|
+
import { usePathname } from "next/navigation"
|
|
10
|
+
|
|
11
|
+
export function NavBar() {
|
|
12
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
|
13
|
+
const pathname = usePathname()
|
|
14
|
+
|
|
15
|
+
// Hide Navbar on specific route
|
|
16
|
+
if (
|
|
17
|
+
pathname?.includes("/pickup/recycle04") ||
|
|
18
|
+
pathname?.startsWith("/gaming") ||
|
|
19
|
+
pathname?.includes("/github/abhisheks04") ||
|
|
20
|
+
pathname?.startsWith("/works/") ||
|
|
21
|
+
pathname?.startsWith("/admin")
|
|
22
|
+
) return null;
|
|
23
|
+
|
|
24
|
+
const handleLogoClick = (e: React.MouseEvent) => {
|
|
25
|
+
if (pathname === "/") {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
window.scrollTo({ top: 0, behavior: "smooth" })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<nav
|
|
34
|
+
className="
|
|
35
|
+
fixed z-50 transition-all duration-300
|
|
36
|
+
top-4 left-8 right-8 h-13 rounded-full
|
|
37
|
+
bg-[#0a0a0a]/60 backdrop-blur-xl
|
|
38
|
+
border border-white/5 shadow-sm
|
|
39
|
+
|
|
40
|
+
pl-4 pr-2
|
|
41
|
+
flex items-center justify-between
|
|
42
|
+
|
|
43
|
+
md:top-0 md:left-0 md:right-0 md:w-full md:h-16 md:rounded-none
|
|
44
|
+
md:bg-[#0a0a0a]/70 md:border-b md:border-white/5 md:shadow-none
|
|
45
|
+
md:px-8
|
|
46
|
+
"
|
|
47
|
+
>
|
|
48
|
+
{/* Left: Logo */}
|
|
49
|
+
<Link
|
|
50
|
+
href="/"
|
|
51
|
+
onClick={handleLogoClick}
|
|
52
|
+
className="flex items-center group"
|
|
53
|
+
>
|
|
54
|
+
<span
|
|
55
|
+
className={cn(
|
|
56
|
+
bolivia.className,
|
|
57
|
+
"text-3xl md:text-4xl text-white/90 group-hover:text-[#007AFF] transition-colors duration-300 tracking-wide pt-1"
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
Abhishek
|
|
61
|
+
</span>
|
|
62
|
+
</Link>
|
|
63
|
+
|
|
64
|
+
{/* Center (Desktop only) */}
|
|
65
|
+
<div className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
|
|
66
|
+
{["Home", "Works", "About"].map(item => (
|
|
67
|
+
<Link
|
|
68
|
+
key={item}
|
|
69
|
+
href={item === "Home" ? "/" : `/${item.toLowerCase()}`}
|
|
70
|
+
onClick={(e) => {
|
|
71
|
+
if (item === "Home" && pathname === "/") {
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
window.scrollTo({ top: 0, behavior: "smooth" })
|
|
74
|
+
}
|
|
75
|
+
}}
|
|
76
|
+
className="px-4 py-2 rounded-full text-sm font-medium text-white/60 hover:text-white hover:bg-white/5 transition-all uppercase tracking-wider"
|
|
77
|
+
>
|
|
78
|
+
{item}
|
|
79
|
+
</Link>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Right Actions */}
|
|
84
|
+
<div className="flex items-center">
|
|
85
|
+
{/* MOBILE ACTION PILL */}
|
|
86
|
+
<div
|
|
87
|
+
className="
|
|
88
|
+
md:hidden
|
|
89
|
+
flex items-center
|
|
90
|
+
gap-1.5
|
|
91
|
+
p-1
|
|
92
|
+
"
|
|
93
|
+
>
|
|
94
|
+
{/* Hamburger */}
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => setIsMenuOpen(true)}
|
|
97
|
+
aria-label="Open Menu"
|
|
98
|
+
className="
|
|
99
|
+
w-8 h-8
|
|
100
|
+
flex items-center justify-center
|
|
101
|
+
transition-colors
|
|
102
|
+
text-white/90
|
|
103
|
+
hover:text-white
|
|
104
|
+
"
|
|
105
|
+
>
|
|
106
|
+
<svg
|
|
107
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
108
|
+
width="16"
|
|
109
|
+
height="16"
|
|
110
|
+
viewBox="0 0 24 24"
|
|
111
|
+
fill="none"
|
|
112
|
+
stroke="currentColor"
|
|
113
|
+
strokeWidth="2"
|
|
114
|
+
strokeLinecap="round"
|
|
115
|
+
strokeLinejoin="round"
|
|
116
|
+
>
|
|
117
|
+
<line x1="4" x2="20" y1="7" y2="7" />
|
|
118
|
+
<line x1="4" x2="20" y1="12" y2="12" />
|
|
119
|
+
<line x1="4" x2="20" y1="17" y2="17" />
|
|
120
|
+
</svg>
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
{/* Social Stories */}
|
|
124
|
+
<div
|
|
125
|
+
className="
|
|
126
|
+
w-10 h-10
|
|
127
|
+
rounded-full
|
|
128
|
+
overflow-hidden
|
|
129
|
+
flex items-center justify-center
|
|
130
|
+
"
|
|
131
|
+
>
|
|
132
|
+
<SocialStories id="mobile" />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* DESKTOP */}
|
|
137
|
+
<div className="hidden md:flex items-center gap-4">
|
|
138
|
+
<div className="w-10 h-10 rounded-full overflow-hidden border border-white/10 hover:border-white/20 transition-colors">
|
|
139
|
+
<SocialStories id="desktop" />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</nav>
|
|
144
|
+
|
|
145
|
+
<MobileMenu isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)} />
|
|
146
|
+
</>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion } from "motion/react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
export function PageTransition({ children }: { children: React.ReactNode }) {
|
|
7
|
+
const pathname = usePathname();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<motion.div
|
|
11
|
+
key={pathname}
|
|
12
|
+
initial={{ opacity: 0, y: 10, filter: "blur(5px)" }}
|
|
13
|
+
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
|
14
|
+
transition={{
|
|
15
|
+
duration: 0.25,
|
|
16
|
+
ease: "easeOut",
|
|
17
|
+
}}
|
|
18
|
+
className="w-full h-full"
|
|
19
|
+
style={{ willChange: "transform, opacity, filter" }}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</motion.div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowRight } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
|
|
7
|
+
interface Error404Props {
|
|
8
|
+
postcardImage?: string;
|
|
9
|
+
postcardAlt?: string;
|
|
10
|
+
curvedTextTop?: string;
|
|
11
|
+
curvedTextBottom?: string;
|
|
12
|
+
heading?: string;
|
|
13
|
+
subtext?: string;
|
|
14
|
+
backButtonLabel?: string;
|
|
15
|
+
backButtonHref?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Error404({
|
|
19
|
+
postcardImage = "https://images.unsplash.com/photo-1485871981521-5b1fd3805eee?q=80&w=2070&auto=format&fit=crop",
|
|
20
|
+
postcardAlt = "New York City Postcard",
|
|
21
|
+
curvedTextTop = "The General Intelligence",
|
|
22
|
+
curvedTextBottom = "Company of New York",
|
|
23
|
+
heading = "(404) Looks like the page you're looking for got lost somewhere.",
|
|
24
|
+
subtext = "But hey — in New York, even the unexpected detours lead somewhere.",
|
|
25
|
+
backButtonLabel = "Back to Home",
|
|
26
|
+
backButtonHref = "/",
|
|
27
|
+
}: Error404Props) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="min-h-screen flex items-center justify-center px-4 py-16 bg-[#0a0a0a]">
|
|
30
|
+
{/* Font import moved to layout or globals is better, but keeping here for single file portability if needed */}
|
|
31
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
32
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
33
|
+
<link href="https://fonts.googleapis.com/css2?family=Doto:wght@100..900&display=swap" rel="stylesheet" />
|
|
34
|
+
|
|
35
|
+
<div className="flex flex-col items-center">
|
|
36
|
+
<div className="relative mb-16">
|
|
37
|
+
<svg
|
|
38
|
+
className="absolute -top-16 -left-12 w-[140px] h-[140px] pointer-events-none z-20 animate-spin-slow"
|
|
39
|
+
viewBox="0 0 140 140"
|
|
40
|
+
>
|
|
41
|
+
<defs>
|
|
42
|
+
<path id="circlePath" d="M 70,70 m -50,0 a 50,50 0 1,1 100,0 a 50,50 0 1,1 -100,0" fill="transparent" />
|
|
43
|
+
</defs>
|
|
44
|
+
<text
|
|
45
|
+
className="text-[11px] fill-white/60 font-serif uppercase"
|
|
46
|
+
style={{ fontWeight: 400, letterSpacing: "0.15em" }}
|
|
47
|
+
>
|
|
48
|
+
<textPath href="#circlePath" startOffset="0%">
|
|
49
|
+
{curvedTextTop} • {curvedTextBottom} •
|
|
50
|
+
</textPath>
|
|
51
|
+
</text>
|
|
52
|
+
</svg>
|
|
53
|
+
|
|
54
|
+
<div className="relative z-10">
|
|
55
|
+
<div
|
|
56
|
+
className="relative p-3 shadow-2xl rotate-[4deg] hover:rotate-0 transition-transform duration-300 bg-[#1a1a1a] border border-white/10"
|
|
57
|
+
>
|
|
58
|
+
<div className="relative overflow-hidden bg-black">
|
|
59
|
+
<img
|
|
60
|
+
src={postcardImage}
|
|
61
|
+
alt={postcardAlt}
|
|
62
|
+
className="w-[360px] h-[220px] object-cover opacity-80"
|
|
63
|
+
/>
|
|
64
|
+
{/* Overlay to make it feel more "postcard-y" */}
|
|
65
|
+
<div className="absolute inset-0 bg-black/20 pointer-events-none" />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Postal cancellation marks */}
|
|
70
|
+
<svg className="absolute -right-16 top-1/2 -translate-y-1/2 w-28 h-20 opacity-30 invert" viewBox="0 0 100 60">
|
|
71
|
+
<path
|
|
72
|
+
d="M 10 15 Q 20 10 30 15 Q 40 20 50 15 Q 60 10 70 15 Q 80 20 90 15"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
strokeWidth="1.5"
|
|
75
|
+
fill="none"
|
|
76
|
+
/>
|
|
77
|
+
<path
|
|
78
|
+
d="M 10 25 Q 20 20 30 25 Q 40 30 50 25 Q 60 20 70 25 Q 80 30 90 25"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
strokeWidth="1.5"
|
|
81
|
+
fill="none"
|
|
82
|
+
/>
|
|
83
|
+
<path
|
|
84
|
+
d="M 10 35 Q 20 30 30 35 Q 40 40 50 35 Q 60 30 70 35 Q 80 40 90 35"
|
|
85
|
+
stroke="currentColor"
|
|
86
|
+
strokeWidth="1.5"
|
|
87
|
+
fill="none"
|
|
88
|
+
/>
|
|
89
|
+
</svg>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="text-center max-w-2xl">
|
|
94
|
+
<h1 className="text-4xl md:text-5xl font-['Doto'] mb-6 text-white text-balance leading-tight">
|
|
95
|
+
{heading}
|
|
96
|
+
</h1>
|
|
97
|
+
<p className="text-white/60 text-base md:text-lg mb-10 font-sans">
|
|
98
|
+
{subtext}
|
|
99
|
+
</p>
|
|
100
|
+
<Button asChild className="rounded-full px-8 py-6 bg-white text-black hover:bg-white/90">
|
|
101
|
+
<Link href={backButtonHref} className="flex items-center gap-2">
|
|
102
|
+
{backButtonLabel}
|
|
103
|
+
<ArrowRight className="w-4 h-4" />
|
|
104
|
+
</Link>
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, createContext, useContext } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
6
|
+
import { Preloader } from "./preloader";
|
|
7
|
+
|
|
8
|
+
// Module-level variable to persist across route changes within the same session
|
|
9
|
+
let globalHasShownPreloader = false;
|
|
10
|
+
|
|
11
|
+
interface PreloaderContextType {
|
|
12
|
+
hasShown: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PreloaderContext = createContext<PreloaderContextType | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
export function PreloaderWrapper({ children }: { children: React.ReactNode }) {
|
|
18
|
+
const pathname = usePathname();
|
|
19
|
+
|
|
20
|
+
// Initialize state based on session to prevent flash.
|
|
21
|
+
// Default to TRUE if not in admin, not shown yet, and on a VALID route.
|
|
22
|
+
const [isVisible, setIsVisible] = useState(() => {
|
|
23
|
+
if (typeof window !== 'undefined' && globalHasShownPreloader) return false;
|
|
24
|
+
|
|
25
|
+
const validRoutes = ["/", "/about", "/works", "/contact"];
|
|
26
|
+
const isDynamicWork = pathname?.startsWith("/works/");
|
|
27
|
+
const isAdmin = pathname?.startsWith("/admin");
|
|
28
|
+
|
|
29
|
+
const isValidRoute = validRoutes.includes(pathname || "") || isDynamicWork || isAdmin;
|
|
30
|
+
|
|
31
|
+
// Skip preloader for admin or invalid (404) routes
|
|
32
|
+
if (isAdmin || !isValidRoute) return false;
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// Remove static splash screen if it exists
|
|
39
|
+
const staticSplash = document.getElementById('static-splash');
|
|
40
|
+
if (staticSplash) {
|
|
41
|
+
staticSplash.style.opacity = '0';
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
staticSplash.remove();
|
|
44
|
+
}, 300);
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
// Handle overflow lock
|
|
50
|
+
if (isVisible) {
|
|
51
|
+
document.body.style.overflow = "hidden";
|
|
52
|
+
} else {
|
|
53
|
+
document.body.style.overflow = "";
|
|
54
|
+
}
|
|
55
|
+
}, [isVisible]);
|
|
56
|
+
|
|
57
|
+
const handleComplete = () => {
|
|
58
|
+
setIsVisible(false);
|
|
59
|
+
globalHasShownPreloader = true;
|
|
60
|
+
document.body.style.overflow = "";
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<PreloaderContext.Provider value={{ hasShown: !isVisible }}>
|
|
65
|
+
<AnimatePresence mode="wait">
|
|
66
|
+
{isVisible && (
|
|
67
|
+
<motion.div
|
|
68
|
+
key="global-preloader"
|
|
69
|
+
initial={{ y: 0 }}
|
|
70
|
+
exit={{
|
|
71
|
+
y: "-100%",
|
|
72
|
+
transition: {
|
|
73
|
+
duration: 1.2, // Slower for more elegance
|
|
74
|
+
ease: [0.83, 0, 0.17, 1] // "Quint" like easing, very dramatic slow start/end
|
|
75
|
+
}
|
|
76
|
+
}}
|
|
77
|
+
className="fixed inset-0 z-[9999] bg-[#050805] flex items-center justify-center"
|
|
78
|
+
>
|
|
79
|
+
<Preloader onComplete={handleComplete} />
|
|
80
|
+
</motion.div>
|
|
81
|
+
)}
|
|
82
|
+
</AnimatePresence>
|
|
83
|
+
|
|
84
|
+
{/*
|
|
85
|
+
Render children immediately.
|
|
86
|
+
We REMOVED the opacity toggle so the browser paints the LCP element
|
|
87
|
+
instantly behind the preloader layer. This fixes the LCP score.
|
|
88
|
+
*/}
|
|
89
|
+
<div className="relative">
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
</PreloaderContext.Provider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const usePreloader = () => {
|
|
97
|
+
const context = useContext(PreloaderContext);
|
|
98
|
+
if (context === undefined) {
|
|
99
|
+
throw new Error("usePreloader must be used within a PreloaderWrapper");
|
|
100
|
+
}
|
|
101
|
+
return context;
|
|
102
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
interface Greeting {
|
|
8
|
+
text: string;
|
|
9
|
+
language: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const greetings: Greeting[] = [
|
|
13
|
+
{ text: "Hello", language: "English" },
|
|
14
|
+
{ text: "Hola", language: "Spanish" },
|
|
15
|
+
{ text: "안녕하세요", language: "Korean" },
|
|
16
|
+
{ text: "Ciao", language: "Italian" },
|
|
17
|
+
{ text: "নমস্কার", language: "Bengali" },
|
|
18
|
+
{ text: "नमस्ते", "language": "Hindi" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
interface PreloaderProps {
|
|
22
|
+
onComplete: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Preloader = ({ onComplete }: PreloaderProps) => {
|
|
26
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
27
|
+
const [dimension, setDimension] = useState({ width: 0, height: 0 });
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setDimension({ width: window.innerWidth, height: window.innerHeight });
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (currentIndex === greetings.length - 1) {
|
|
35
|
+
// Smoothest exit: hold the last word slightly longer then complete
|
|
36
|
+
const timeout = setTimeout(() => {
|
|
37
|
+
onComplete();
|
|
38
|
+
}, 1000); // 1s hold for the last word
|
|
39
|
+
return () => clearTimeout(timeout);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isMobile = window.innerWidth < 768;
|
|
43
|
+
// 1.4s per word as requested
|
|
44
|
+
const stepDuration = 1400;
|
|
45
|
+
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
setCurrentIndex((prev) => prev + 1);
|
|
48
|
+
}, stepDuration);
|
|
49
|
+
|
|
50
|
+
return () => clearTimeout(timeout);
|
|
51
|
+
}, [currentIndex, onComplete]);
|
|
52
|
+
|
|
53
|
+
const textVariants = {
|
|
54
|
+
initial: {
|
|
55
|
+
opacity: 0,
|
|
56
|
+
y: 40, // Increased movement for more "drama"
|
|
57
|
+
filter: "blur(12px)", // Stronger blur
|
|
58
|
+
},
|
|
59
|
+
animate: {
|
|
60
|
+
opacity: 1,
|
|
61
|
+
y: 0,
|
|
62
|
+
filter: "blur(0px)",
|
|
63
|
+
transition: {
|
|
64
|
+
// Reduced duration to fit within 1.4s interval (Wait Mode: Exit + Enter + Static = Interval)
|
|
65
|
+
// 0.4s Exit + 0.6s Enter = 1.0s. Leaves 0.4s for static readability.
|
|
66
|
+
duration: 0.6,
|
|
67
|
+
ease: [0.25, 1, 0.5, 1] as const,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
exit: {
|
|
71
|
+
opacity: 0,
|
|
72
|
+
y: -40, // Match entrance movement
|
|
73
|
+
filter: "blur(12px)",
|
|
74
|
+
transition: {
|
|
75
|
+
duration: 0.4,
|
|
76
|
+
ease: [0.25, 1, 0.5, 1] as const
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-[#050805] cursor-none overflow-hidden">
|
|
83
|
+
{/*
|
|
84
|
+
Use absolute positioning for the text container to prevent any flex-based layout shifts
|
|
85
|
+
during size changes (though text is mostly centered, this is safer).
|
|
86
|
+
*/}
|
|
87
|
+
<AnimatePresence mode="wait">
|
|
88
|
+
{greetings[currentIndex] && (
|
|
89
|
+
<motion.p
|
|
90
|
+
key={currentIndex}
|
|
91
|
+
variants={textVariants}
|
|
92
|
+
initial="initial"
|
|
93
|
+
animate="animate"
|
|
94
|
+
exit="exit"
|
|
95
|
+
className="absolute text-4xl md:text-7xl font-light tracking-tight text-white font-sans mix-blend-difference"
|
|
96
|
+
style={{ willChange: "transform, opacity, filter" }}
|
|
97
|
+
>
|
|
98
|
+
{greetings[currentIndex].text}
|
|
99
|
+
</motion.p>
|
|
100
|
+
)}
|
|
101
|
+
</AnimatePresence>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
2
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
|
|
5
|
+
export interface Contributor {
|
|
6
|
+
id?: string
|
|
7
|
+
name: string
|
|
8
|
+
avatar_url: string
|
|
9
|
+
fallback: string
|
|
10
|
+
social_url?: string
|
|
11
|
+
role?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProjectContributorsProps {
|
|
15
|
+
contributors: Contributor[]
|
|
16
|
+
className?: string
|
|
17
|
+
size?: 'sm' | 'md' | 'lg'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ProjectContributors = ({ contributors, className = '', size = 'md' }: ProjectContributorsProps) => {
|
|
21
|
+
const sizeClasses = {
|
|
22
|
+
sm: 'size-8',
|
|
23
|
+
md: 'size-10',
|
|
24
|
+
lg: 'size-12'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={`flex -space-x-3 ${className}`}>
|
|
29
|
+
<TooltipProvider delayDuration={0}>
|
|
30
|
+
{contributors.map((contributor, index) => (
|
|
31
|
+
<Tooltip key={index}>
|
|
32
|
+
<TooltipTrigger asChild>
|
|
33
|
+
<Link
|
|
34
|
+
href={contributor.social_url || '#'}
|
|
35
|
+
target={contributor.social_url ? "_blank" : "_self"}
|
|
36
|
+
className={`relative transition-transform hover:z-10 hover:scale-110`}
|
|
37
|
+
>
|
|
38
|
+
<Avatar className={`ring-background ring-2 ${sizeClasses[size]}`}>
|
|
39
|
+
<AvatarImage src={contributor.avatar_url} alt={contributor.name} className="object-cover" />
|
|
40
|
+
<AvatarFallback>{contributor.fallback}</AvatarFallback>
|
|
41
|
+
</Avatar>
|
|
42
|
+
</Link>
|
|
43
|
+
</TooltipTrigger>
|
|
44
|
+
<TooltipContent>
|
|
45
|
+
<div className="flex flex-col items-center">
|
|
46
|
+
<span className="font-semibold text-xs">{contributor.name}</span>
|
|
47
|
+
{contributor.role && <span className="text-[10px] text-muted-foreground">{contributor.role}</span>}
|
|
48
|
+
</div>
|
|
49
|
+
</TooltipContent>
|
|
50
|
+
</Tooltip>
|
|
51
|
+
))}
|
|
52
|
+
</TooltipProvider>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default ProjectContributors
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const scrollAreaVariants = cva("relative overflow-hidden", {
|
|
9
|
+
variants: {
|
|
10
|
+
orientation: {
|
|
11
|
+
vertical: "h-full",
|
|
12
|
+
horizontal: "w-full",
|
|
13
|
+
both: "h-full w-full",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
orientation: "vertical",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const scrollBarVariants = cva("flex touch-none select-none transition-colors", {
|
|
22
|
+
variants: {
|
|
23
|
+
orientation: {
|
|
24
|
+
vertical: "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
25
|
+
horizontal: "h-2.5 w-full border-t border-t-transparent p-[1px]",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
orientation: "vertical",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export interface ScrollAreaProps
|
|
34
|
+
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>,
|
|
35
|
+
VariantProps<typeof scrollAreaVariants> {
|
|
36
|
+
scrollHideDelay?: number;
|
|
37
|
+
type?: "auto" | "always" | "scroll" | "hover";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ScrollArea = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
42
|
+
ScrollAreaProps
|
|
43
|
+
>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
className,
|
|
47
|
+
children,
|
|
48
|
+
orientation,
|
|
49
|
+
scrollHideDelay = 600,
|
|
50
|
+
type = "hover",
|
|
51
|
+
...props
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) => (
|
|
55
|
+
<ScrollAreaPrimitive.Root
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn(scrollAreaVariants({ orientation }), className)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
61
|
+
{children}
|
|
62
|
+
</ScrollAreaPrimitive.Viewport>
|
|
63
|
+
<ScrollBar
|
|
64
|
+
orientation="vertical"
|
|
65
|
+
type={type}
|
|
66
|
+
scrollHideDelay={scrollHideDelay}
|
|
67
|
+
/>
|
|
68
|
+
{(orientation === "horizontal" || orientation === "both") && (
|
|
69
|
+
<ScrollBar
|
|
70
|
+
orientation="horizontal"
|
|
71
|
+
type={type}
|
|
72
|
+
scrollHideDelay={scrollHideDelay}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
<ScrollAreaPrimitive.Corner />
|
|
76
|
+
</ScrollAreaPrimitive.Root>
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
81
|
+
|
|
82
|
+
interface ScrollBarProps
|
|
83
|
+
extends React.ComponentPropsWithoutRef<
|
|
84
|
+
typeof ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
85
|
+
> {
|
|
86
|
+
scrollHideDelay?: number;
|
|
87
|
+
type?: "auto" | "always" | "scroll" | "hover";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ScrollBar = React.forwardRef<
|
|
91
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
92
|
+
ScrollBarProps
|
|
93
|
+
>(
|
|
94
|
+
(
|
|
95
|
+
{ className, orientation = "vertical", scrollHideDelay, type, ...props },
|
|
96
|
+
ref
|
|
97
|
+
) => {
|
|
98
|
+
return (
|
|
99
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
100
|
+
ref={ref}
|
|
101
|
+
orientation={orientation}
|
|
102
|
+
className={cn(
|
|
103
|
+
scrollBarVariants({ orientation }),
|
|
104
|
+
"hover:bg-accent",
|
|
105
|
+
className
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border hover:bg-foreground/30 transition-colors" />
|
|
110
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
116
|
+
|
|
117
|
+
export { ScrollArea, ScrollBar, scrollAreaVariants };
|