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.
- package/README.md +5 -0
- package/package.json +82 -0
- package/src/app/fonts.css +8 -0
- package/src/app/globals.css +192 -0
- package/src/components/fonts/welcome.ttf +0 -0
- package/src/components/layout/header.tsx +50 -0
- package/src/components/layout/protected-layout.tsx +39 -0
- package/src/components/layout/sidebar-nav.tsx +150 -0
- package/src/components/logo.tsx +43 -0
- package/src/components/markdown-renderer.tsx +19 -0
- package/src/components/page-transition.tsx +45 -0
- package/src/components/password-gate.tsx +178 -0
- package/src/components/portal-page.tsx +471 -0
- package/src/components/profile-card.tsx +107 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/carousel.tsx +262 -0
- package/src/components/ui/chart.tsx +365 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/form.tsx +178 -0
- package/src/components/ui/index.ts +38 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/menubar.tsx +256 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +44 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +790 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +21 -0
- package/src/components/ui/toast.tsx +129 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/upload-form.tsx +243 -0
- package/src/components/writeup-card.tsx +53 -0
- package/src/components/writeups-list.tsx +60 -0
- package/src/lib/actions.ts +21 -0
- package/src/lib/placeholder-images.json +88 -0
- package/src/lib/placeholder-images.ts +14 -0
- package/src/lib/types.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/writeups.ts +76 -0
- 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 & 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
|
+
}
|