m33n4n-site 0.1.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.
Files changed (60) hide show
  1. package/README.md +5 -0
  2. package/package.json +82 -0
  3. package/src/app/fonts.css +8 -0
  4. package/src/app/globals.css +192 -0
  5. package/src/components/fonts/welcome.ttf +0 -0
  6. package/src/components/layout/header.tsx +50 -0
  7. package/src/components/layout/protected-layout.tsx +39 -0
  8. package/src/components/layout/sidebar-nav.tsx +150 -0
  9. package/src/components/logo.tsx +43 -0
  10. package/src/components/markdown-renderer.tsx +19 -0
  11. package/src/components/page-transition.tsx +45 -0
  12. package/src/components/password-gate.tsx +178 -0
  13. package/src/components/portal-page.tsx +471 -0
  14. package/src/components/profile-card.tsx +107 -0
  15. package/src/components/ui/accordion.tsx +58 -0
  16. package/src/components/ui/alert-dialog.tsx +141 -0
  17. package/src/components/ui/alert.tsx +59 -0
  18. package/src/components/ui/avatar.tsx +50 -0
  19. package/src/components/ui/badge.tsx +36 -0
  20. package/src/components/ui/button.tsx +56 -0
  21. package/src/components/ui/calendar.tsx +70 -0
  22. package/src/components/ui/card.tsx +79 -0
  23. package/src/components/ui/carousel.tsx +262 -0
  24. package/src/components/ui/chart.tsx +365 -0
  25. package/src/components/ui/checkbox.tsx +30 -0
  26. package/src/components/ui/collapsible.tsx +11 -0
  27. package/src/components/ui/dialog.tsx +122 -0
  28. package/src/components/ui/dropdown-menu.tsx +200 -0
  29. package/src/components/ui/form.tsx +178 -0
  30. package/src/components/ui/index.ts +38 -0
  31. package/src/components/ui/input.tsx +22 -0
  32. package/src/components/ui/label.tsx +26 -0
  33. package/src/components/ui/menubar.tsx +256 -0
  34. package/src/components/ui/popover.tsx +31 -0
  35. package/src/components/ui/progress.tsx +28 -0
  36. package/src/components/ui/radio-group.tsx +44 -0
  37. package/src/components/ui/scroll-area.tsx +48 -0
  38. package/src/components/ui/select.tsx +160 -0
  39. package/src/components/ui/separator.tsx +31 -0
  40. package/src/components/ui/sheet.tsx +140 -0
  41. package/src/components/ui/sidebar.tsx +790 -0
  42. package/src/components/ui/skeleton.tsx +15 -0
  43. package/src/components/ui/slider.tsx +28 -0
  44. package/src/components/ui/switch.tsx +29 -0
  45. package/src/components/ui/table.tsx +117 -0
  46. package/src/components/ui/tabs.tsx +55 -0
  47. package/src/components/ui/textarea.tsx +21 -0
  48. package/src/components/ui/toast.tsx +129 -0
  49. package/src/components/ui/toaster.tsx +35 -0
  50. package/src/components/ui/tooltip.tsx +30 -0
  51. package/src/components/upload-form.tsx +243 -0
  52. package/src/components/writeup-card.tsx +53 -0
  53. package/src/components/writeups-list.tsx +60 -0
  54. package/src/lib/actions.ts +21 -0
  55. package/src/lib/placeholder-images.json +88 -0
  56. package/src/lib/placeholder-images.ts +14 -0
  57. package/src/lib/types.ts +14 -0
  58. package/src/lib/utils.ts +6 -0
  59. package/src/lib/writeups.ts +76 -0
  60. package/tailwind.config.ts +110 -0
@@ -0,0 +1,178 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, type ReactNode } from 'react';
4
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { ShieldAlert, Timer, Key } from 'lucide-react';
6
+ import { Alert, AlertDescription, AlertTitle } from './ui/alert';
7
+ import { Input } from './ui/input';
8
+ import { Button } from './ui/button';
9
+ import { useToast } from '@/hooks/use-toast';
10
+ import { Separator } from './ui/separator';
11
+
12
+
13
+ const CountdownTimer = ({ releaseDate }: { releaseDate: string }) => {
14
+ const calculateTimeLeft = () => {
15
+ const difference = +new Date(releaseDate) - +new Date();
16
+ let timeLeft = {};
17
+
18
+ if (difference > 0) {
19
+ timeLeft = {
20
+ days: Math.floor(difference / (1000 * 60 * 60 * 24)),
21
+ hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
22
+ minutes: Math.floor((difference / 1000 / 60) % 60),
23
+ seconds: Math.floor((difference / 1000) % 60),
24
+ };
25
+ }
26
+ return timeLeft;
27
+ };
28
+
29
+ const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());
30
+
31
+ useEffect(() => {
32
+ const timer = setTimeout(() => {
33
+ setTimeLeft(calculateTimeLeft());
34
+ }, 1000);
35
+
36
+ return () => clearTimeout(timer);
37
+ });
38
+
39
+ const timerComponents: ReactNode[] = [];
40
+
41
+ Object.keys(timeLeft).forEach((interval) => {
42
+ // @ts-ignore
43
+ if (!timeLeft[interval] && interval !== 'seconds') {
44
+ return;
45
+ }
46
+
47
+ timerComponents.push(
48
+ <div key={interval} className="flex flex-col items-center">
49
+ <span className="text-3xl font-bold font-code text-primary">
50
+ {/* @ts-ignore */}
51
+ {String(timeLeft[interval]).padStart(2, '0')}
52
+ </span>
53
+ <span className="text-xs text-muted-foreground uppercase">{interval}</span>
54
+ </div>
55
+ );
56
+ });
57
+
58
+ return (
59
+ <Card className="w-full max-w-md shadow-lg bg-card/80 backdrop-blur-sm border-primary/20 flex-1">
60
+ <CardHeader className="text-center">
61
+ <div className="mx-auto bg-primary/10 rounded-full p-3 w-fit mb-4">
62
+ <Timer className="w-8 h-8 text-primary" />
63
+ </div>
64
+ <CardTitle className="text-2xl font-headline text-primary">Content Locked</CardTitle>
65
+ <CardDescription>This write-up will be available after the countdown.</CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <div className="flex justify-center gap-4">
69
+ {timerComponents.length ? timerComponents : <span>Content is now available!</span>}
70
+ </div>
71
+ </CardContent>
72
+ </Card>
73
+ );
74
+ };
75
+
76
+ export default function PasswordGate({
77
+ children,
78
+ image,
79
+ releaseDate,
80
+ tokens,
81
+ }: {
82
+ children: ReactNode,
83
+ image: ReactNode,
84
+ releaseDate?: string,
85
+ tokens?: string[],
86
+ }) {
87
+ const [isTimeUp, setIsTimeUp] = useState(!releaseDate || new Date(releaseDate) < new Date());
88
+ const [isUnlocked, setIsUnlocked] = useState(isTimeUp);
89
+ const [tokenInput, setTokenInput] = useState('');
90
+ const { toast } = useToast();
91
+
92
+ useEffect(() => {
93
+ if (isUnlocked) return;
94
+ if (!releaseDate) {
95
+ setIsTimeUp(true);
96
+ setIsUnlocked(true);
97
+ return;
98
+ };
99
+
100
+ const interval = setInterval(() => {
101
+ if (new Date(releaseDate) < new Date()) {
102
+ setIsTimeUp(true);
103
+ setIsUnlocked(true);
104
+ clearInterval(interval);
105
+ }
106
+ }, 1000);
107
+
108
+ return () => clearInterval(interval);
109
+ }, [releaseDate, isUnlocked]);
110
+
111
+ const handleTokenUnlock = () => {
112
+ if (tokens && tokens.includes(tokenInput)) {
113
+ setIsUnlocked(true);
114
+ toast({
115
+ title: "Access Granted",
116
+ description: "You have unlocked the write-up with a token.",
117
+ });
118
+ } else {
119
+ toast({
120
+ variant: "destructive",
121
+ title: "Access Denied",
122
+ description: "The token you entered is incorrect.",
123
+ });
124
+ }
125
+ };
126
+
127
+ if (!isUnlocked) {
128
+ return (
129
+ <div className="flex flex-col items-center justify-center py-12 gap-8">
130
+ {image}
131
+ <Alert variant="default" className="max-w-md border-blue-500/30 bg-blue-900/20">
132
+ <ShieldAlert className="h-4 w-4 text-blue-400" />
133
+ <AlertTitle className="text-blue-300">Terms of Service Notice</AlertTitle>
134
+ <AlertDescription>
135
+ To comply with Hack The Box's terms, write-ups for active challenges are time-locked. This content will be automatically available once the official release date is reached.
136
+ </AlertDescription>
137
+ </Alert>
138
+
139
+ <div className="w-full max-w-4xl flex flex-col md:flex-row items-center justify-center gap-8">
140
+ <CountdownTimer releaseDate={releaseDate!} />
141
+ {tokens && tokens.length > 0 && (
142
+ <>
143
+ <Separator orientation="vertical" className="h-48 hidden md:block" />
144
+ <Separator orientation="horizontal" className="w-48 md:hidden" />
145
+
146
+ <Card className="w-full max-w-md shadow-lg bg-card/80 backdrop-blur-sm border-primary/20 flex-1">
147
+ <CardHeader className="text-center pb-4">
148
+ <div className="mx-auto bg-primary/10 rounded-full p-3 w-fit mb-2">
149
+ <Key className="w-6 h-6 text-primary" />
150
+ </div>
151
+ <CardTitle className="text-xl font-headline text-primary">Unlock with Token</CardTitle>
152
+ <CardDescription>Have a token? Bypass the timer now.</CardDescription>
153
+ </CardHeader>
154
+ <CardContent>
155
+ <div className="flex gap-2">
156
+ <Input
157
+ type="text"
158
+ placeholder="Enter your token..."
159
+ value={tokenInput}
160
+ onChange={(e) => setTokenInput(e.target.value)}
161
+ className="bg-background/50 border-primary/30"
162
+ onKeyDown={(e) => e.key === 'Enter' && handleTokenUnlock()}
163
+ />
164
+ <Button onClick={handleTokenUnlock} variant="outline" className="border-primary/50">
165
+ Unlock
166
+ </Button>
167
+ </div>
168
+ </CardContent>
169
+ </Card>
170
+ </>
171
+ )}
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ return <>{children}</>;
178
+ }
@@ -0,0 +1,471 @@
1
+ 'use client';
2
+ import {
3
+ BookOpen,
4
+ ArrowDown,
5
+ Github,
6
+ Linkedin,
7
+ Key,
8
+ ChevronUp,
9
+ ChevronDown,
10
+ Mail,
11
+ Lock,
12
+ } from 'lucide-react';
13
+ import Link from 'next/link';
14
+ import Image from 'next/image';
15
+ import { Badge } from './ui/badge';
16
+ import { getPlaceholderImage } from '@/lib/placeholder-images';
17
+ import { Button } from './ui/button';
18
+ import { useState, useEffect, useMemo } from 'react';
19
+ import Logo from './logo';
20
+ import { Input } from './ui/input';
21
+ import { useRouter } from 'next/navigation';
22
+ import { Alert, AlertDescription } from './ui/alert';
23
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
24
+ import { Card, CardContent, CardHeader } from './ui/card';
25
+ import { AnimatePresence, motion } from 'framer-motion';
26
+ import { useAuth } from '@/context/auth-context';
27
+ import {
28
+ Dialog,
29
+ DialogContent,
30
+ DialogDescription,
31
+ DialogHeader,
32
+ DialogTitle,
33
+ DialogTrigger,
34
+ } from '@/components/ui/dialog';
35
+
36
+
37
+ const allTerminalContent = [
38
+ { command: "whoami", output: "m33n4n" },
39
+ { command: "id", output: "uid=1000(m33n4n) gid=1000(m33n4n) groups=1000(m33n4n),4(adm),27(sudo)", desktopOnly: true },
40
+ { command: "uname -a", output: "Linux m33n4n-host 5.4.0-generic #86-Ubuntu SMP x86_64 GNU/Linux", desktopOnly: true },
41
+ { command: "ps -ef | grep ssh", output: "root 1025 1 0 08:30 ? 00:00:00 /usr/sbin/sshd -D", desktopOnly: true },
42
+ { command: "nc -lvp 4444", output: "listening on [any] 4444 ..." }
43
+ ];
44
+
45
+ const TerminalCommand = () => {
46
+ const [contentIndex, setContentIndex] = useState(0);
47
+ const [displayedCommand, setDisplayedCommand] = useState('');
48
+ const [displayedOutput, setDisplayedOutput] = useState('');
49
+ const [animationPhase, setAnimationPhase] = useState('typing'); // typing, outputting, clearing
50
+ const [isMobile, setIsMobile] = useState(false);
51
+
52
+ useEffect(() => {
53
+ const checkMobile = () => setIsMobile(window.innerWidth < 768);
54
+ checkMobile();
55
+ window.addEventListener('resize', checkMobile);
56
+ return () => window.removeEventListener('resize', checkMobile);
57
+ }, []);
58
+
59
+ const terminalContent = useMemo(() => {
60
+ if (isMobile) {
61
+ return allTerminalContent.filter(item => !item.desktopOnly);
62
+ }
63
+ return allTerminalContent;
64
+ }, [isMobile]);
65
+
66
+ useEffect(() => {
67
+ if (terminalContent.length === 0) return;
68
+
69
+ const currentItem = terminalContent[contentIndex % terminalContent.length];
70
+
71
+ const handleAnimation = () => {
72
+ switch (animationPhase) {
73
+ case 'typing':
74
+ if (displayedCommand.length < currentItem.command.length) {
75
+ setDisplayedCommand(currentItem.command.substring(0, displayedCommand.length + 1));
76
+ } else {
77
+ // Pause after typing command, then show output
78
+ setTimeout(() => setAnimationPhase('outputting'), 1000);
79
+ }
80
+ break;
81
+
82
+ case 'outputting':
83
+ setDisplayedOutput(currentItem.output);
84
+ // Pause with output shown, then clear
85
+ setTimeout(() => setAnimationPhase('clearing'), 3000);
86
+ break;
87
+
88
+ case 'clearing':
89
+ setDisplayedCommand('');
90
+ setDisplayedOutput('');
91
+ setContentIndex((prevIndex) => (prevIndex + 1));
92
+ setAnimationPhase('typing');
93
+ break;
94
+ }
95
+ };
96
+
97
+ const typingSpeed = animationPhase === 'typing' ? 120 : 50;
98
+ const timeout = setTimeout(handleAnimation, typingSpeed);
99
+
100
+ return () => clearTimeout(timeout);
101
+ }, [displayedCommand, animationPhase, contentIndex, terminalContent]);
102
+
103
+ if (terminalContent.length === 0) return null;
104
+
105
+ return (
106
+ <div className="w-full max-w-2xl bg-card border border-primary/20 rounded-lg p-4 font-code text-sm shadow-lg shadow-black/30 h-[110px]">
107
+ <div className="flex items-center space-x-2 text-xs text-muted-foreground mb-4">
108
+ <div className="flex space-x-1.5">
109
+ <div className="w-3 h-3 rounded-full bg-red-500"></div>
110
+ <div className="w-3 h-3 rounded-full bg-yellow-500"></div>
111
+ <div className="w-3 h-3 rounded-full bg-green-500"></div>
112
+ </div>
113
+ <span>terminal@m33n4n:~</span>
114
+ </div>
115
+ <div>
116
+ <span className="text-primary">$</span>
117
+ <span className="ml-2 text-white">{displayedCommand}</span>
118
+ {animationPhase === 'typing' && <span className="ml-1 h-4 w-1 animate-pulse bg-green-400"></span>}
119
+ {displayedOutput && (
120
+ <div className="text-muted-foreground mt-2 whitespace-pre-wrap">{displayedOutput}</div>
121
+ )}
122
+ </div>
123
+ </div>
124
+ );
125
+ };
126
+
127
+ const BlogAccessChallenge = ({ onUnlock }: { onUnlock: () => void }) => {
128
+ const [key, setKey] = useState('');
129
+ const [error, setError] = useState(false);
130
+ const correctKey = "LET_ME_IN";
131
+ const encodedKey = "TEVUX01FX0lO";
132
+ const [isMobile, setIsMobile] = useState(false);
133
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
134
+
135
+
136
+ useEffect(() => {
137
+ const checkMobile = () => setIsMobile(window.innerWidth < 768);
138
+ checkMobile();
139
+ window.addEventListener('resize', checkMobile);
140
+ return () => window.removeEventListener('resize', checkMobile);
141
+ }, []);
142
+
143
+ const handleAccess = () => {
144
+ if (key === correctKey) {
145
+ setError(false);
146
+ onUnlock();
147
+ setIsDialogOpen(false);
148
+ } else {
149
+ setError(true);
150
+ setTimeout(() => setError(false), 2000);
151
+ }
152
+ };
153
+
154
+ return (
155
+ <div className="w-full max-w-md mx-auto space-y-4">
156
+ <div className="text-center font-code text-sm text-muted-foreground">
157
+ <p>Decode the key to enter:</p>
158
+ <p className="text-primary font-bold tracking-wider">{encodedKey}</p>
159
+ </div>
160
+
161
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
162
+ <div className="flex justify-center">
163
+ <DialogTrigger asChild>
164
+ <Button
165
+ variant="outline"
166
+ className="w-48 bg-white/10 backdrop-blur-sm border-white/20 text-white hover:bg-white/20 transition-all duration-300 group"
167
+ >
168
+ <Lock className="h-5 w-5 mr-2 transition-transform duration-300 group-hover:rotate-[-10deg]" />
169
+ Unlock
170
+ </Button>
171
+ </DialogTrigger>
172
+ </div>
173
+ <DialogContent className="sm:max-w-[425px] bg-card border-primary/30">
174
+ <DialogHeader>
175
+ <DialogTitle className="text-primary font-headline">Enter Access Key</DialogTitle>
176
+ <DialogDescription>
177
+ Enter the decoded key below to proceed.
178
+ </DialogDescription>
179
+ </DialogHeader>
180
+ <div className="grid gap-4 py-4">
181
+ <div className="flex items-center gap-2">
182
+ <Input
183
+ type="text"
184
+ placeholder="Enter decoded key..."
185
+ value={key}
186
+ onChange={(e) => setKey(e.target.value)}
187
+ onKeyDown={(e) => e.key === 'Enter' && handleAccess()}
188
+ className="bg-background/80 font-code text-center border-primary/30 focus:border-primary"
189
+ />
190
+ {isMobile ? (
191
+ <Button variant="outline" size="icon" onClick={handleAccess} className="border-primary/30 hover:bg-primary/20">
192
+ <Key className="h-5 w-5" />
193
+ </Button>
194
+ ) : (
195
+ <Button variant="outline" onClick={handleAccess} className="border-primary/30 hover:bg-primary/20">
196
+ <Key className="h-5 w-5 mr-2" />
197
+ Exploit
198
+ </Button>
199
+ )}
200
+ </div>
201
+ {error && (
202
+ <Alert variant="destructive" className="p-2 text-center text-xs">
203
+ <AlertDescription>Invalid Key</AlertDescription>
204
+ </Alert>
205
+ )}
206
+ </div>
207
+ </DialogContent>
208
+ </Dialog>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ const HackTheBoxIcon = (props: React.SVGProps<SVGSVGElement>) => (
214
+ <Image
215
+ src="https://shadowv0id.vercel.app/hackthebox.png"
216
+ alt="Hackthebox Logo"
217
+ width={24}
218
+ height={24}
219
+ {...props}
220
+ />
221
+ );
222
+
223
+ const TryHackMeIcon = (props: React.SVGProps<SVGSVGElement>) => (
224
+ <Image
225
+ src="https://tryhackme.com/img/favicon.png"
226
+ alt="TryHackMe Logo"
227
+ width={24}
228
+ height={24}
229
+ {...props}
230
+ />
231
+ );
232
+
233
+ const socialLinks = [
234
+ { name: 'GitHub', href: '#', icon: Github },
235
+ { name: 'LinkedIn', href: '#', icon: Linkedin },
236
+ { name: 'HackTheBox', href: '#', icon: HackTheBoxIcon },
237
+ { name: 'TryHackMe', href: '#', icon: TryHackMeIcon },
238
+ ];
239
+
240
+ const ContactCard = () => {
241
+ const [isOpen, setIsOpen] = useState(false);
242
+
243
+ const pgpKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
244
+ xjMEaTvlWxYJKwYBBAHaRw8BAQdAw5A1S92Fn/t06CFPALL7GHtRl0Bb+44b
245
+ QtOr6XSDsPXNHE0zM040TiA8bjB0bTMzbjRuQHByb3Rvbi5tZT7CjAQQFgoA
246
+ PgWCaTvlWwQLCQcICZBtZdEQaiarJQMVCAoEFgACAQIZAQKbAwIeARYhBMbQ
247
+ b/zFZHNveV8Fxm1l0RBqJqslAAAI4AEA9gKonRldgEUxW997CsNtm2IuZ7Rm
248
+ XblciHoC5A+/algBAMm0j+mjQwNLBjX6Ihg4l597BCJQSHZpIg8jFGcUe1YH
249
+ zjgEaTvlWxIKKwYBBAGXVQEFAQEHQFHHHPIN77kFdjbyNhyZuJCF8giZHMEi
250
+ 9wRPUKXgK3hEAwEIB8J4BBgWCgAqBYJpO+VbCZBtZdEQaiarJQKbDBYhBMbQ
251
+ b/zFZHNveV8Fxm1l0RBqJqslAABFngD7BcM0at5ucvsMuSy5IkSERRpzUxh2
252
+ QKZbuijdUtLGX5sA/35+Eq7H9rrC1a8KMmSgdVej+D9rqXFrCpReD9Eap9QK
253
+ =GR7J
254
+ -----END PGP PUBLIC KEY BLOCK-----`;
255
+
256
+ return (
257
+ <Collapsible
258
+ open={isOpen}
259
+ onOpenChange={setIsOpen}
260
+ className="fixed bottom-4 right-4 z-50 w-[300px]"
261
+ >
262
+ <Card className="bg-card/80 backdrop-blur-md border-primary/20 shadow-lg">
263
+ <CollapsibleTrigger asChild>
264
+ <Button variant="ghost" className="w-full justify-between p-4 h-auto">
265
+ <div className='flex items-center gap-2'>
266
+ <Mail className="h-5 w-5 text-primary" />
267
+ <span className="font-semibold text-primary">Contact Me (PGP)</span>
268
+ </div>
269
+ {isOpen ? <ChevronDown className="h-5 w-5" /> : <ChevronUp className="h-5 w-5" />}
270
+ </Button>
271
+ </CollapsibleTrigger>
272
+ <CollapsibleContent>
273
+ <CardContent className="p-4 pt-0 text-left">
274
+ <a href="mailto:not0day@proton.me" className="font-code text-primary hover:underline block mb-2 text-sm">
275
+ n0tm33n4n@proton.me
276
+ </a>
277
+ <pre className="mt-2 p-2 bg-black/20 rounded-md text-xs whitespace-pre-wrap break-words font-code text-muted-foreground overflow-x-auto">
278
+ {pgpKey}
279
+ </pre>
280
+ </CardContent>
281
+ </CollapsibleContent>
282
+ </Card>
283
+ </Collapsible>
284
+ );
285
+ };
286
+
287
+ export default function PortalPage() {
288
+ const router = useRouter();
289
+ const { isUnlocked, setUnlocked } = useAuth();
290
+ const [isRedirecting, setIsRedirecting] = useState(false);
291
+
292
+ const headerImage = getPlaceholderImage('first-image');
293
+ const secondImage = getPlaceholderImage('second-image');
294
+ const thirdImage = getPlaceholderImage('third-image');
295
+ const fourthImage = getPlaceholderImage('fourth-image');
296
+ const middleImage = getPlaceholderImage('middle-image');
297
+ const beforePasswdImage = getPlaceholderImage('before-passwd');
298
+ const afterPasswdImage = getPlaceholderImage('after-passwd');
299
+ const chevronDownImage = getPlaceholderImage('chevron-down');
300
+
301
+ const handleUnlock = () => {
302
+ setUnlocked(true);
303
+ setIsRedirecting(true);
304
+ };
305
+
306
+ useEffect(() => {
307
+ if (isRedirecting) {
308
+ const timer = setTimeout(() => {
309
+ router.push('/blog');
310
+ }, 1500); // Delay redirection to show the unlocked state
311
+ return () => clearTimeout(timer);
312
+ }
313
+ }, [isRedirecting, router]);
314
+
315
+
316
+ return (
317
+ <div className="flex flex-col items-center justify-center min-h-screen p-4 overflow-hidden">
318
+ <div className="absolute top-4 left-4 z-20">
319
+ <Logo showText={false} />
320
+ </div>
321
+ {/* Background Elements */}
322
+ <div className="absolute inset-0 z-0">
323
+ {middleImage && (
324
+ <>
325
+ <Image
326
+ src={middleImage.imageUrl}
327
+ alt="Background decorative element"
328
+ fill
329
+ sizes="100vw"
330
+ priority
331
+ className="object-cover opacity-40"
332
+ data-ai-hint={middleImage.imageHint}
333
+ />
334
+ <div className="absolute inset-0 bg-black/60"></div>
335
+ </>
336
+ )}
337
+ </div>
338
+
339
+ {/* Foreground Content */}
340
+ <div className="relative z-10 w-full flex flex-col items-center">
341
+ {headerImage && (
342
+ <div className="relative w-48 h-40 mb-2 rounded-lg overflow-hidden shadow-lg mx-auto">
343
+ <Image
344
+ src={headerImage.imageUrl}
345
+ alt={'Header image'}
346
+ fill
347
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
348
+ className="object-cover"
349
+ data-ai-hint={headerImage.imageHint}
350
+ />
351
+ </div>
352
+ )}
353
+ <div className="relative w-full max-w-3xl mt-[-3rem] mx-auto">
354
+ {secondImage && (
355
+ <div className="absolute bottom-7 left-[-90px] translate-y-1/3 hidden md:block">
356
+ <Image
357
+ src={secondImage.imageUrl}
358
+ alt="Decorative element"
359
+ width={150}
360
+ height={150}
361
+ className="object-contain"
362
+ data-ai-hint={secondImage.imageHint}
363
+ />
364
+ </div>
365
+ )}
366
+ <div className="relative z-10 w-full bg-card/70 backdrop-blur-md border border-primary/20 rounded-2xl p-4 shadow-2xl shadow-black/30">
367
+ <div className="text-center">
368
+ <h1 className="glitch font-glitch text-5xl font-bold" data-text="Welcome">
369
+ Welcome
370
+ </h1>
371
+ {/* <p className="text-xl text-muted-foreground mt-2">
372
+ Security Researcher &amp; Penetration Tester
373
+ </p> */}
374
+ </div>
375
+
376
+ <div className="my-2 relative h-28">
377
+ <AnimatePresence>
378
+ <motion.div
379
+ key={isUnlocked ? 'unlocked' : 'locked'}
380
+ initial={{ opacity: 0, scale: 0.9 }}
381
+ animate={{ opacity: 1, scale: 1 }}
382
+ exit={{ opacity: 0, scale: 0.9 }}
383
+ transition={{ duration: 0.5 }}
384
+ className="absolute inset-0 flex flex-col items-center justify-center"
385
+ >
386
+ {isUnlocked && afterPasswdImage ? (
387
+ <>
388
+ <Image
389
+ src={afterPasswdImage.imageUrl}
390
+ alt="Challenge solved"
391
+ width={100}
392
+ height={100}
393
+ className="object-contain"
394
+ data-ai-hint={afterPasswdImage.imageHint}
395
+ />
396
+ <motion.p
397
+ initial={{ opacity: 0 }}
398
+ animate={{ opacity: 1 }}
399
+ transition={{ delay: 0.5, duration: 0.5 }}
400
+ className="text-primary font-bold mt-2"
401
+ >
402
+ Enjoy the blogs
403
+ </motion.p>
404
+ </>
405
+ ) : beforePasswdImage ? (
406
+ <Image
407
+ src={beforePasswdImage.imageUrl}
408
+ alt="Challenge to solve"
409
+ width={100}
410
+ height={100}
411
+ className="object-contain"
412
+ data-ai-hint={beforePasswdImage.imageHint}
413
+ />
414
+ ) : null}
415
+ </motion.div>
416
+ </AnimatePresence>
417
+ </div>
418
+
419
+
420
+ {!isUnlocked && (
421
+ <div className="my-4">
422
+ <BlogAccessChallenge onUnlock={handleUnlock} />
423
+ </div>
424
+ )}
425
+
426
+ <div className="flex items-center justify-center gap-4 my-4">
427
+ {socialLinks.map(({ name, href, icon: Icon }) => (
428
+ <Link href={href} key={name} target="_blank" rel="noopener noreferrer">
429
+ <Button variant="ghost" size="icon" className="group">
430
+ <Icon className="h-6 w-6 text-muted-foreground transition-all duration-300 group-hover:text-primary group-hover:scale-110" />
431
+ <span className="sr-only">{name}</span>
432
+ </Button>
433
+ </Link>
434
+ ))}
435
+ </div>
436
+
437
+ <div className="mt-4 px-4 md:px-8">
438
+ <TerminalCommand />
439
+ </div>
440
+ </div>
441
+ {thirdImage && (
442
+ <div className="absolute bottom-7 right-[-90px] translate-y-1/3 hidden md:block">
443
+ <Image
444
+ src={thirdImage.imageUrl}
445
+ alt="Decorative element"
446
+ width={150}
447
+ height={150}
448
+ className="object-contain"
449
+ data-ai-hint={thirdImage.imageHint}
450
+ />
451
+ </div>
452
+ )}
453
+ {fourthImage && (
454
+ <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/3 hidden md:block">
455
+ <Image
456
+ src={fourthImage.imageUrl}
457
+ alt="Decorative element"
458
+ width={100}
459
+ height={100}
460
+ className="object-contain"
461
+ data-ai-hint={fourthImage.imageHint}
462
+ />
463
+ </div>
464
+ )}
465
+ </div>
466
+ </div>
467
+
468
+ <ContactCard />
469
+ </div>
470
+ );
471
+ }