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
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "m33n4n-site",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "src/components/ui/index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/components",
|
|
8
|
+
"src/lib",
|
|
9
|
+
"src/app/globals.css",
|
|
10
|
+
"src/app/fonts.css",
|
|
11
|
+
"tailwind.config.ts"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "next dev --turbopack -p 9002",
|
|
15
|
+
"genkit:dev": "genkit start -- tsx src/ai/dev.ts",
|
|
16
|
+
"genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
|
|
17
|
+
"build": "NODE_ENV=production next build",
|
|
18
|
+
"start": "next start",
|
|
19
|
+
"lint": "next lint",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@genkit-ai/google-genai": "^1.20.0",
|
|
24
|
+
"@genkit-ai/next": "^1.20.0",
|
|
25
|
+
"@hookform/resolvers": "^4.1.3",
|
|
26
|
+
"@radix-ui/react-accordion": "^1.2.3",
|
|
27
|
+
"@radix-ui/react-alert-dialog": "^1.1.6",
|
|
28
|
+
"@radix-ui/react-avatar": "^1.1.3",
|
|
29
|
+
"@radix-ui/react-checkbox": "^1.1.4",
|
|
30
|
+
"@radix-ui/react-collapsible": "^1.1.11",
|
|
31
|
+
"@radix-ui/react-dialog": "^1.1.6",
|
|
32
|
+
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
|
33
|
+
"@radix-ui/react-label": "^2.1.2",
|
|
34
|
+
"@radix-ui/react-menubar": "^1.1.6",
|
|
35
|
+
"@radix-ui/react-popover": "^1.1.6",
|
|
36
|
+
"@radix-ui/react-progress": "^1.1.2",
|
|
37
|
+
"@radix-ui/react-radio-group": "^1.2.3",
|
|
38
|
+
"@radix-ui/react-scroll-area": "^1.2.3",
|
|
39
|
+
"@radix-ui/react-select": "^2.1.6",
|
|
40
|
+
"@radix-ui/react-separator": "^1.1.2",
|
|
41
|
+
"@radix-ui/react-slider": "^1.2.3",
|
|
42
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
43
|
+
"@radix-ui/react-switch": "^1.1.3",
|
|
44
|
+
"@radix-ui/react-tabs": "^1.1.3",
|
|
45
|
+
"@radix-ui/react-toast": "^1.2.6",
|
|
46
|
+
"@radix-ui/react-tooltip": "^1.1.8",
|
|
47
|
+
"class-variance-authority": "^0.7.1",
|
|
48
|
+
"clsx": "^2.1.1",
|
|
49
|
+
"date-fns": "^3.6.0",
|
|
50
|
+
"dotenv": "^16.5.0",
|
|
51
|
+
"embla-carousel-react": "^8.6.0",
|
|
52
|
+
"firebase": "^11.9.1",
|
|
53
|
+
"framer-motion": "^11.5.7",
|
|
54
|
+
"genkit": "^1.20.0",
|
|
55
|
+
"gray-matter": "^4.0.3",
|
|
56
|
+
"lucide-react": "^0.475.0",
|
|
57
|
+
"next": "15.3.8",
|
|
58
|
+
"patch-package": "^8.0.0",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-day-picker": "^8.10.1",
|
|
61
|
+
"react-dom": "^18.3.1",
|
|
62
|
+
"react-hook-form": "^7.54.2",
|
|
63
|
+
"react-markdown": "^9.0.1",
|
|
64
|
+
"recharts": "^2.15.1",
|
|
65
|
+
"rehype-raw": "^7.0.0",
|
|
66
|
+
"remark-gfm": "^4.0.0",
|
|
67
|
+
"resend": "^3.5.0",
|
|
68
|
+
"tailwind-merge": "^3.0.1",
|
|
69
|
+
"tailwindcss-animate": "^1.0.7",
|
|
70
|
+
"zod": "^3.24.2"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@tailwindcss/typography": "^0.5.15",
|
|
74
|
+
"@types/node": "^20",
|
|
75
|
+
"@types/react": "^18",
|
|
76
|
+
"@types/react-dom": "^18",
|
|
77
|
+
"genkit-cli": "^1.20.0",
|
|
78
|
+
"postcss": "^8",
|
|
79
|
+
"tailwindcss": "^3.4.1",
|
|
80
|
+
"typescript": "^5"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
|
|
2
|
+
@tailwind base;
|
|
3
|
+
@tailwind components;
|
|
4
|
+
@tailwind utilities;
|
|
5
|
+
|
|
6
|
+
@layer base {
|
|
7
|
+
:root {
|
|
8
|
+
--background: 216 21% 16%; /* 🌑 LURKING DARK — #1F2531 */
|
|
9
|
+
--foreground: 216 27% 95%; /* ⚪ WHISKER WHITE — #EAEEF4 */
|
|
10
|
+
|
|
11
|
+
--card: 216 21% 18%; /* Slightly lighter than background */
|
|
12
|
+
--card-foreground: 216 27% 95%;
|
|
13
|
+
|
|
14
|
+
--popover: 216 21% 16%;
|
|
15
|
+
--popover-foreground: 216 27% 95%;
|
|
16
|
+
|
|
17
|
+
--primary: 42 99% 55%; /* 🟡 FAT CAT MUSTARD — #FEB81C */
|
|
18
|
+
--primary-foreground: 216 21% 16%;
|
|
19
|
+
|
|
20
|
+
--secondary: 216 21% 25%; /* Darker version of background */
|
|
21
|
+
--secondary-foreground: 216 27% 95%;
|
|
22
|
+
|
|
23
|
+
--muted: 216 21% 25%;
|
|
24
|
+
--muted-foreground: 216 15% 65%;
|
|
25
|
+
|
|
26
|
+
--accent: 216 21% 25%;
|
|
27
|
+
--accent-foreground: 216 27% 95%;
|
|
28
|
+
|
|
29
|
+
--destructive: 359 88% 54%; /* 🔴 CLAW RED — #F12325 */
|
|
30
|
+
--destructive-foreground: 216 27% 95%;
|
|
31
|
+
|
|
32
|
+
--border: 42 99% 45%; /* Mustard for borders */
|
|
33
|
+
--input: 216 21% 25%;
|
|
34
|
+
--ring: 42 99% 55%; /* Mustard for focus rings */
|
|
35
|
+
|
|
36
|
+
--radius: 0.5rem;
|
|
37
|
+
--chart-1: 42 99% 55%;
|
|
38
|
+
--chart-2: 42 89% 50%;
|
|
39
|
+
--chart-3: 42 79% 45%;
|
|
40
|
+
--chart-4: 359 78% 49%;
|
|
41
|
+
--chart-5: 359 88% 54%;
|
|
42
|
+
|
|
43
|
+
--sidebar-background: 216 21% 16%;
|
|
44
|
+
--sidebar-foreground: 216 27% 85%;
|
|
45
|
+
--sidebar-primary: 42 99% 55%;
|
|
46
|
+
--sidebar-primary-foreground: 216 21% 16%;
|
|
47
|
+
--sidebar-accent: 216 21% 25%;
|
|
48
|
+
--sidebar-accent-foreground: 216 27% 95%;
|
|
49
|
+
--sidebar-border: 42 99% 55%;
|
|
50
|
+
--sidebar-ring: 42 99% 55%;
|
|
51
|
+
|
|
52
|
+
--code-background: 216 21% 20%;
|
|
53
|
+
--terminal-header: 216 21% 18%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dark {
|
|
57
|
+
--background: 216 21% 16%; /* 🌑 LURKING DARK — #1F2531 */
|
|
58
|
+
--foreground: 216 27% 95%; /* ⚪ WHISKER WHITE — #EAEEF4 */
|
|
59
|
+
|
|
60
|
+
--card: 216 21% 18%; /* Slightly lighter than background */
|
|
61
|
+
--card-foreground: 216 27% 95%;
|
|
62
|
+
|
|
63
|
+
--popover: 216 21% 16%;
|
|
64
|
+
--popover-foreground: 216 27% 95%;
|
|
65
|
+
|
|
66
|
+
--primary: 42 99% 55%; /* 🟡 FAT CAT MUSTARD — #FEB81C */
|
|
67
|
+
--primary-foreground: 216 21% 16%;
|
|
68
|
+
|
|
69
|
+
--secondary: 216 21% 25%; /* Darker version of background */
|
|
70
|
+
--secondary-foreground: 216 27% 95%;
|
|
71
|
+
|
|
72
|
+
--muted: 216 21% 25%;
|
|
73
|
+
--muted-foreground: 216 15% 65%;
|
|
74
|
+
|
|
75
|
+
--accent: 216 21% 25%;
|
|
76
|
+
--accent-foreground: 216 27% 95%;
|
|
77
|
+
|
|
78
|
+
--destructive: 359 88% 54%; /* 🔴 CLAW RED — #F12325 */
|
|
79
|
+
--destructive-foreground: 216 27% 95%;
|
|
80
|
+
|
|
81
|
+
--border: 42 99% 45%; /* Mustard for borders */
|
|
82
|
+
--input: 216 21% 25%;
|
|
83
|
+
--ring: 42 99% 55%; /* Mustard for focus rings */
|
|
84
|
+
|
|
85
|
+
--chart-1: 42 99% 55%;
|
|
86
|
+
--chart-2: 42 89% 50%;
|
|
87
|
+
--chart-3: 42 79% 45%;
|
|
88
|
+
--chart-4: 359 78% 49%;
|
|
89
|
+
--chart-5: 359 88% 54%;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@layer base {
|
|
94
|
+
* {
|
|
95
|
+
@apply border-border;
|
|
96
|
+
}
|
|
97
|
+
body {
|
|
98
|
+
@apply bg-background text-foreground;
|
|
99
|
+
cursor: url('https://shadowv0id.vercel.app/mouse-cursor.png'), auto;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@layer components {
|
|
105
|
+
.prose {
|
|
106
|
+
@apply text-foreground/90 max-w-none;
|
|
107
|
+
}
|
|
108
|
+
.prose h1,
|
|
109
|
+
.prose h2,
|
|
110
|
+
.prose h3,
|
|
111
|
+
.prose h4,
|
|
112
|
+
.prose h5,
|
|
113
|
+
.prose h6 {
|
|
114
|
+
@apply text-primary font-headline tracking-tighter;
|
|
115
|
+
}
|
|
116
|
+
.prose h1 { @apply text-4xl; }
|
|
117
|
+
.prose h2 { @apply text-3xl; }
|
|
118
|
+
.prose h3 { @apply text-2xl; }
|
|
119
|
+
|
|
120
|
+
.prose a {
|
|
121
|
+
@apply text-primary/90 transition-colors hover:text-primary;
|
|
122
|
+
}
|
|
123
|
+
.prose strong {
|
|
124
|
+
@apply text-foreground;
|
|
125
|
+
}
|
|
126
|
+
.prose blockquote {
|
|
127
|
+
@apply relative bg-secondary/30 border border-primary/30 rounded-lg pl-12 pr-4 py-4 text-muted-foreground not-italic;
|
|
128
|
+
}
|
|
129
|
+
.prose blockquote::before {
|
|
130
|
+
content: "i";
|
|
131
|
+
@apply absolute left-4 top-4 flex items-center justify-center h-6 w-6 rounded-full bg-primary/80 text-primary-foreground font-bold text-sm;
|
|
132
|
+
}
|
|
133
|
+
.prose code {
|
|
134
|
+
@apply bg-[var(--code-background)] text-primary px-1.5 py-1 rounded-md font-code text-[0.9em] before:content-[''] after:content-[''];
|
|
135
|
+
}
|
|
136
|
+
.prose pre {
|
|
137
|
+
@apply bg-card/80 border border-primary/20 rounded-lg overflow-x-auto font-code shadow-lg relative p-4 pt-12;
|
|
138
|
+
}
|
|
139
|
+
.prose pre::before {
|
|
140
|
+
content: '';
|
|
141
|
+
@apply absolute top-0 left-0 right-0 h-8 bg-card rounded-t-lg border-b border-border/50;
|
|
142
|
+
}
|
|
143
|
+
.prose pre::after {
|
|
144
|
+
content: '';
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 0.75rem;
|
|
147
|
+
left: 1rem;
|
|
148
|
+
width: 0.75rem;
|
|
149
|
+
height: 0.75rem;
|
|
150
|
+
border-radius: 9999px;
|
|
151
|
+
background-color: #ff5f56;
|
|
152
|
+
box-shadow: 1.25rem 0 0 #ffbd2e, 2.5rem 0 0 #27c93f;
|
|
153
|
+
}
|
|
154
|
+
.prose pre code {
|
|
155
|
+
@apply bg-transparent p-0 text-[0.9em] font-normal text-foreground/90;
|
|
156
|
+
}
|
|
157
|
+
.prose ul > li::marker {
|
|
158
|
+
@apply text-primary;
|
|
159
|
+
}
|
|
160
|
+
.prose ol > li::marker {
|
|
161
|
+
@apply text-primary;
|
|
162
|
+
}
|
|
163
|
+
.prose hr {
|
|
164
|
+
@apply border-border;
|
|
165
|
+
}
|
|
166
|
+
.prose table {
|
|
167
|
+
@apply w-full my-6;
|
|
168
|
+
}
|
|
169
|
+
.prose th {
|
|
170
|
+
@apply border border-border px-4 py-2 text-left font-bold;
|
|
171
|
+
@apply bg-secondary;
|
|
172
|
+
}
|
|
173
|
+
.prose td {
|
|
174
|
+
@apply border border-border px-4 py-2;
|
|
175
|
+
}
|
|
176
|
+
.prose img {
|
|
177
|
+
@apply rounded-lg;
|
|
178
|
+
}
|
|
179
|
+
.highlight-word {
|
|
180
|
+
@apply text-red-500 font-bold;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@layer utilities {
|
|
185
|
+
.glitch {
|
|
186
|
+
color: hsl(var(--primary));
|
|
187
|
+
text-shadow:
|
|
188
|
+
-0.05em 0.025em 0 rgba(255, 0, 0, 0.75),
|
|
189
|
+
0.05em -0.025em 0 rgba(0, 255, 255, 0.75),
|
|
190
|
+
0.025em 0.05em 0 rgba(0, 255, 0, 0.75);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { Search } from 'lucide-react';
|
|
5
|
+
import { Input } from '../ui/input';
|
|
6
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import Logo from '../logo';
|
|
9
|
+
import Image from 'next/image';
|
|
10
|
+
import { getPlaceholderImage } from '@/lib/placeholder-images';
|
|
11
|
+
import { SidebarTrigger } from '../ui/sidebar';
|
|
12
|
+
|
|
13
|
+
export default function Header() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const searchParams = useSearchParams();
|
|
16
|
+
const [searchValue, setSearchValue] = useState(searchParams.get('q') || '');
|
|
17
|
+
const profileImage = getPlaceholderImage('profile-pic');
|
|
18
|
+
|
|
19
|
+
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
router.push(`/writeups?q=${searchValue}`);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setSearchValue(searchParams.get('q') || '');
|
|
26
|
+
}, [searchParams]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<header className="sticky top-0 z-10 flex h-16 items-center justify-between gap-4 border-b border-black/20 bg-background/70 px-4 backdrop-blur-lg md:px-6">
|
|
30
|
+
<div className="flex items-center gap-4">
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="w-full flex-1 md:w-auto md:flex-none">
|
|
35
|
+
<form onSubmit={handleSearch}>
|
|
36
|
+
<div className="relative">
|
|
37
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
38
|
+
<Input
|
|
39
|
+
type="search"
|
|
40
|
+
placeholder="Search write-ups..."
|
|
41
|
+
className="w-full appearance-none bg-background/50 pl-9 md:w-64 lg:w-96"
|
|
42
|
+
value={searchValue}
|
|
43
|
+
onChange={(e) => setSearchValue(e.target.value)}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</form>
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAuth } from '@/context/auth-context';
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
import Loading from '@/app/loading';
|
|
7
|
+
|
|
8
|
+
export default function ProtectedLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
const { isUnlocked, isLoading } = useAuth();
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Wait until loading is finished before checking auth state
|
|
19
|
+
if (isLoading) return;
|
|
20
|
+
|
|
21
|
+
// If not unlocked and not on the portal page, redirect to portal
|
|
22
|
+
if (!isUnlocked && pathname !== '/') {
|
|
23
|
+
router.replace('/');
|
|
24
|
+
}
|
|
25
|
+
}, [isLoading, isUnlocked, pathname, router]);
|
|
26
|
+
|
|
27
|
+
// While checking auth state, show a loading screen
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return <Loading />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If we're on a protected route and not unlocked, show loading while redirecting
|
|
33
|
+
if (!isUnlocked && pathname !== '/') {
|
|
34
|
+
return <Loading />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If unlocked or on the public portal page, show the content
|
|
38
|
+
return <>{children}</>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import {
|
|
6
|
+
SidebarContent,
|
|
7
|
+
SidebarFooter,
|
|
8
|
+
SidebarGroup,
|
|
9
|
+
SidebarGroupLabel,
|
|
10
|
+
SidebarHeader,
|
|
11
|
+
SidebarMenu,
|
|
12
|
+
SidebarMenuButton,
|
|
13
|
+
SidebarMenuItem,
|
|
14
|
+
SidebarSeparator,
|
|
15
|
+
useSidebar,
|
|
16
|
+
} from '../ui/sidebar';
|
|
17
|
+
import { Archive, FileText, User, Flag, Users, Rss, Key } from 'lucide-react';
|
|
18
|
+
import React, { useEffect, useState } from 'react';
|
|
19
|
+
import { cn } from '@/lib/utils';
|
|
20
|
+
import { getAllCategories } from '@/lib/writeups';
|
|
21
|
+
import Image from 'next/image';
|
|
22
|
+
import { getPlaceholderImage } from '@/lib/placeholder-images';
|
|
23
|
+
|
|
24
|
+
export default function SidebarNav() {
|
|
25
|
+
const pathname = usePathname();
|
|
26
|
+
const searchParams = useSearchParams();
|
|
27
|
+
const activeCategory = searchParams.get('category');
|
|
28
|
+
const activePath = searchParams.get('path');
|
|
29
|
+
const [categories, setCategories] = useState<string[]>([]);
|
|
30
|
+
const { state } = useSidebar();
|
|
31
|
+
const profileImage = getPlaceholderImage('sidebar-profile-pic');
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setCategories(['HTB', 'TryHackMe', 'Offsec']);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const isActive = (path: string) => {
|
|
39
|
+
if ((path === '/writeups' && activeCategory) || (path === '/writeups' && activePath)) return false;
|
|
40
|
+
return pathname === path || (path !== '/' && pathname.startsWith(path));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<SidebarHeader className="p-4 flex items-center justify-center">
|
|
46
|
+
{profileImage && (
|
|
47
|
+
<Link href="/" className="flex flex-col items-center gap-2">
|
|
48
|
+
<Image
|
|
49
|
+
src={profileImage.imageUrl}
|
|
50
|
+
alt="Profile"
|
|
51
|
+
width={120}
|
|
52
|
+
height={120}
|
|
53
|
+
className="transition-all duration-300 group-data-[collapsible=icon]:scale-100"
|
|
54
|
+
/>
|
|
55
|
+
<span className={cn(
|
|
56
|
+
'text-lg font-bold font-headline tracking-tighter text-primary transition-opacity duration-200 group-data-[collapsible=icon]:opacity-0'
|
|
57
|
+
)}>
|
|
58
|
+
M33N4N
|
|
59
|
+
</span>
|
|
60
|
+
</Link>
|
|
61
|
+
)}
|
|
62
|
+
</SidebarHeader>
|
|
63
|
+
<SidebarContent>
|
|
64
|
+
<SidebarGroup>
|
|
65
|
+
<SidebarGroupLabel>Main</SidebarGroupLabel>
|
|
66
|
+
<SidebarMenu>
|
|
67
|
+
<SidebarMenuItem>
|
|
68
|
+
<SidebarMenuButton
|
|
69
|
+
asChild
|
|
70
|
+
isActive={isActive('/blog')}
|
|
71
|
+
tooltip="Blog"
|
|
72
|
+
>
|
|
73
|
+
<Link href="/blog">
|
|
74
|
+
<Rss />
|
|
75
|
+
Blog
|
|
76
|
+
</Link>
|
|
77
|
+
</SidebarMenuButton>
|
|
78
|
+
</SidebarMenuItem>
|
|
79
|
+
<SidebarMenuItem>
|
|
80
|
+
<SidebarMenuButton
|
|
81
|
+
asChild
|
|
82
|
+
isActive={isActive('/writeups')}
|
|
83
|
+
tooltip="Write-ups"
|
|
84
|
+
>
|
|
85
|
+
<Link href="/writeups">
|
|
86
|
+
<FileText />
|
|
87
|
+
Write-ups
|
|
88
|
+
</Link>
|
|
89
|
+
</SidebarMenuButton>
|
|
90
|
+
</SidebarMenuItem>
|
|
91
|
+
<SidebarMenuItem>
|
|
92
|
+
<SidebarMenuButton
|
|
93
|
+
asChild
|
|
94
|
+
isActive={isActive('/archive')}
|
|
95
|
+
tooltip="Archive"
|
|
96
|
+
>
|
|
97
|
+
<Link href="/archive">
|
|
98
|
+
<Archive />
|
|
99
|
+
Archive
|
|
100
|
+
</Link>
|
|
101
|
+
</SidebarMenuButton>
|
|
102
|
+
</SidebarMenuItem>
|
|
103
|
+
</SidebarMenu>
|
|
104
|
+
</SidebarGroup>
|
|
105
|
+
<SidebarSeparator />
|
|
106
|
+
<SidebarGroup>
|
|
107
|
+
<SidebarGroupLabel>Community</SidebarGroupLabel>
|
|
108
|
+
<SidebarMenu>
|
|
109
|
+
<SidebarMenuItem>
|
|
110
|
+
<SidebarMenuButton
|
|
111
|
+
asChild
|
|
112
|
+
tooltip="Discord"
|
|
113
|
+
size="sm"
|
|
114
|
+
isActive={false}
|
|
115
|
+
>
|
|
116
|
+
<Link href="#" target="_blank">
|
|
117
|
+
<Users />
|
|
118
|
+
Discord
|
|
119
|
+
</Link>
|
|
120
|
+
</SidebarMenuButton>
|
|
121
|
+
</SidebarMenuItem>
|
|
122
|
+
</SidebarMenu>
|
|
123
|
+
</SidebarGroup>
|
|
124
|
+
<SidebarSeparator />
|
|
125
|
+
<SidebarGroup>
|
|
126
|
+
<SidebarGroupLabel>Categories</SidebarGroupLabel>
|
|
127
|
+
<SidebarMenu>
|
|
128
|
+
{categories.map(category => (
|
|
129
|
+
<SidebarMenuItem key={category}>
|
|
130
|
+
<SidebarMenuButton
|
|
131
|
+
asChild
|
|
132
|
+
isActive={
|
|
133
|
+
pathname === '/writeups' && activeCategory === category
|
|
134
|
+
}
|
|
135
|
+
size="sm"
|
|
136
|
+
tooltip={category}
|
|
137
|
+
>
|
|
138
|
+
<Link href={`/writeups?category=${category}`}>
|
|
139
|
+
<FileText />
|
|
140
|
+
{category}
|
|
141
|
+
</Link>
|
|
142
|
+
</SidebarMenuButton>
|
|
143
|
+
</SidebarMenuItem>
|
|
144
|
+
))}
|
|
145
|
+
</SidebarMenu>
|
|
146
|
+
</SidebarGroup>
|
|
147
|
+
</SidebarContent>
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
type LogoProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
textClassName?: string;
|
|
8
|
+
showText?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function Logo({
|
|
12
|
+
className,
|
|
13
|
+
textClassName,
|
|
14
|
+
showText = true,
|
|
15
|
+
}: LogoProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Link
|
|
18
|
+
href="/"
|
|
19
|
+
className={cn(
|
|
20
|
+
'flex flex-col items-center justify-center text-primary group',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
<Image
|
|
25
|
+
src="https://shadowv0id.vercel.app/cat-logo.png"
|
|
26
|
+
alt="M33N4N Logo"
|
|
27
|
+
width={28}
|
|
28
|
+
height={28}
|
|
29
|
+
className="transition-transform duration-300 group-hover:rotate-12"
|
|
30
|
+
/>
|
|
31
|
+
{showText && (
|
|
32
|
+
<span
|
|
33
|
+
className={cn(
|
|
34
|
+
'text-lg font-bold font-headline tracking-tighter text-primary transition-colors group-hover:text-primary/80',
|
|
35
|
+
textClassName
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
M33N4N
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</Link>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import ReactMarkdown from 'react-markdown';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
|
|
7
|
+
export default function MarkdownRenderer({ content }: { content: string }) {
|
|
8
|
+
return (
|
|
9
|
+
<ReactMarkdown
|
|
10
|
+
remarkPlugins={[remarkGfm]}
|
|
11
|
+
rehypePlugins={[rehypeRaw]}
|
|
12
|
+
components={{
|
|
13
|
+
// Customize components here if needed to match the theme
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
{content}
|
|
17
|
+
</ReactMarkdown>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
const pageVariants = {
|
|
7
|
+
initial: {
|
|
8
|
+
opacity: 0,
|
|
9
|
+
y: '10vh',
|
|
10
|
+
},
|
|
11
|
+
in: {
|
|
12
|
+
opacity: 1,
|
|
13
|
+
y: 0,
|
|
14
|
+
transition: {
|
|
15
|
+
duration: 0.5,
|
|
16
|
+
ease: 'anticipate',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
out: {
|
|
20
|
+
opacity: 0,
|
|
21
|
+
y: '-10vh',
|
|
22
|
+
transition: {
|
|
23
|
+
duration: 0.3,
|
|
24
|
+
ease: 'anticipate',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default function PageTransition({ children }: { children: React.ReactNode }) {
|
|
30
|
+
const pathname = usePathname();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
34
|
+
<motion.div
|
|
35
|
+
key={pathname}
|
|
36
|
+
variants={pageVariants}
|
|
37
|
+
initial="initial"
|
|
38
|
+
animate="in"
|
|
39
|
+
exit="out"
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</motion.div>
|
|
43
|
+
</AnimatePresence>
|
|
44
|
+
);
|
|
45
|
+
}
|