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
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Firebase Studio
2
+
3
+ This is a NextJS starter in Firebase Studio.
4
+
5
+ To get started, take a look at src/app/page.tsx.
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,8 @@
1
+
2
+ @font-face {
3
+ font-family: 'WelcomeFont';
4
+ src: url('/fonts/welcome.ttf') format('truetype');
5
+ font-weight: normal;
6
+ font-style: normal;
7
+ font-display: swap;
8
+ }
@@ -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
+ }