Compare commits
3 Commits
7e04d3091a
...
5d54506fe5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d54506fe5 | ||
|
|
d1b0577b69 | ||
|
|
c5adea5672 |
BIN
public/media/art/screensaver/Artboard – 2.png
Normal file
BIN
public/media/art/screensaver/Artboard – 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/media/art/screensaver/Artboard – 4.png
Normal file
BIN
public/media/art/screensaver/Artboard – 4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/media/art/screensaver/ScreenSaver-1.png
Normal file
BIN
public/media/art/screensaver/ScreenSaver-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/media/art/screensaver/ScreenSaver-2.png
Normal file
BIN
public/media/art/screensaver/ScreenSaver-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/media/art/screensaver/ScreenSaver-3.png
Normal file
BIN
public/media/art/screensaver/ScreenSaver-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -1,121 +1,23 @@
|
||||
import DigitalArt from "@/components/Art";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { artItems } from "@/data/creativeData";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Digital Art",
|
||||
description: "Explore my digital art collection featuring creative works through digital canvases and new media. Discover the intersection of technology and artistic expression.",
|
||||
description:
|
||||
"Explore my digital art collection featuring creative works through digital canvases and new media. Discover the intersection of technology and artistic expression.",
|
||||
openGraph: {
|
||||
title: "Digital Art | Mainasara Tsowa",
|
||||
description: "Explore my digital art collection featuring creative works through digital canvases and new media.",
|
||||
description:
|
||||
"Explore my digital art collection featuring creative works through digital canvases and new media.",
|
||||
url: "https://mainasara.dev/digital-art",
|
||||
},
|
||||
twitter: {
|
||||
title: "Digital Art | Mainasara Tsowa",
|
||||
description: "Explore my digital art collection featuring creative works through digital canvases and new media.",
|
||||
description:
|
||||
"Explore my digital art collection featuring creative works through digital canvases and new media.",
|
||||
},
|
||||
};
|
||||
|
||||
export default function DigitalArtPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-pink-50 to-yellow-50 font-mono">
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<header className="text-center mb-16">
|
||||
<div className="mb-8">
|
||||
<div className="w-32 h-32 mx-auto bg-black rounded-full flex items-center justify-center border-4 border-purple-300">
|
||||
<div className="text-4xl font-bold text-purple-300">🎨</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 uppercase tracking-wide">
|
||||
Digital Art
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700 max-w-2xl mx-auto leading-relaxed font-semibold">
|
||||
Exploring creativity through digital canvases and new media
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav className="mb-16">
|
||||
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/digital-art"
|
||||
className="border-purple-200 bg-purple-300 px-4 py-2 border-2 text-black"
|
||||
>
|
||||
Digital Art
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/stories"
|
||||
className="text-black font-bold hover:bg-yellow-200 px-4 py-2 transition-colors border-2 border-black hover:border-yellow-400"
|
||||
>
|
||||
Stories
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/thoughts"
|
||||
className="text-black font-bold hover:bg-teal-200 px-4 py-2 transition-colors border-2 border-black hover:border-teal-400"
|
||||
>
|
||||
Thoughts
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="space-y-8">
|
||||
{artItems.map((art) => (
|
||||
<article
|
||||
key={art.id}
|
||||
className="bg-white border-4 border-black rounded-none p-8 shadow-brutal"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className="md:w-1/3">
|
||||
<div className="bg-purple-100 border-2 border-black rounded-none w-full h-48 flex items-center justify-center">
|
||||
<span className="text-6xl">🎨</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4 uppercase">
|
||||
{art.title}
|
||||
</h2>
|
||||
<p className="text-gray-800 mb-4 leading-relaxed">
|
||||
{art.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<span className="bg-purple-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{art.medium}
|
||||
</span>
|
||||
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{new Date(art.createdAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</main>
|
||||
|
||||
<footer className="mt-20 text-center">
|
||||
<div className="bg-black text-white p-4 font-bold">
|
||||
<p className="text-sm uppercase">Built with brutalist pastels</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <DigitalArt />;
|
||||
}
|
||||
|
||||
@@ -13,60 +13,67 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://mainasara.dev'),
|
||||
metadataBase: new URL("https://mainasara.dev"),
|
||||
title: {
|
||||
default: 'Mainasara Tsowa - Developer & Cybersecurity Expert',
|
||||
template: '%s | Mainasara Tsowa'
|
||||
default: "Mainasara Tsowa - Developer & Cybersecurity Expert",
|
||||
template: "%s | Mainasara Tsowa",
|
||||
},
|
||||
description: 'Passionate developer and cybersecurity expert building secure, elegant digital solutions. Explore projects, creative writing, and technical insights.',
|
||||
keywords: ['developer', 'cybersecurity', 'full-stack', 'web development', 'security', 'programming', 'digital art', 'creative writing'],
|
||||
authors: [{ name: 'Mainasara Tsowa' }],
|
||||
creator: 'Mainasara Tsowa',
|
||||
publisher: 'Mainasara Tsowa',
|
||||
robots: 'index, follow',
|
||||
description:
|
||||
"Passionate developer and cybersecurity expert building secure, elegant digital solutions. Explore projects, creative writing, and technical insights.",
|
||||
keywords: [
|
||||
"developer",
|
||||
"cybersecurity",
|
||||
"full-stack",
|
||||
"web development",
|
||||
"security",
|
||||
"programming",
|
||||
"digital art",
|
||||
"creative writing",
|
||||
],
|
||||
authors: [{ name: "Mainasara Tsowa" }],
|
||||
creator: "Mainasara Tsowa",
|
||||
publisher: "Mainasara Tsowa",
|
||||
robots: "index, follow",
|
||||
openGraph: {
|
||||
title: 'Mainasara Tsowa - Developer & Cybersecurity Expert',
|
||||
description: 'Passionate developer and cybersecurity expert building secure, elegant digital solutions.',
|
||||
url: 'https://mainasara.dev',
|
||||
siteName: 'Mainasara Tsowa',
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
title: "Mainasara Tsowa - Developer & Cybersecurity Expert",
|
||||
description:
|
||||
"Passionate developer and cybersecurity expert building secure, elegant digital solutions.",
|
||||
url: "https://mainasara.dev",
|
||||
siteName: "Mainasara Tsowa",
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: '/media/me/me.jpeg',
|
||||
url: "/media/me/me.jpeg",
|
||||
width: 800,
|
||||
height: 800,
|
||||
alt: 'Mainasara Tsowa',
|
||||
alt: "Mainasara Tsowa",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Mainasara Tsowa - Developer & Cybersecurity Expert',
|
||||
description: 'Passionate developer and cybersecurity expert building secure, elegant digital solutions.',
|
||||
images: ['/media/me/me.jpeg'],
|
||||
creator: '@mainasara',
|
||||
card: "summary_large_image",
|
||||
title: "Mainasara Tsowa - Developer & Cybersecurity Expert",
|
||||
description:
|
||||
"Passionate developer and cybersecurity expert building secure, elegant digital solutions.",
|
||||
images: ["/media/me/me.jpeg"],
|
||||
creator: "@neutrino2211",
|
||||
},
|
||||
alternates: {
|
||||
canonical: 'https://mainasara.dev',
|
||||
canonical: "https://mainasara.dev",
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.ico' },
|
||||
{ url: '/icon.png', type: 'image/png' },
|
||||
],
|
||||
apple: [
|
||||
{ url: '/apple-icon.png' },
|
||||
],
|
||||
icon: [{ url: "/favicon.ico" }, { url: "/icon.png", type: "image/png" }],
|
||||
apple: [{ url: "/apple-icon.png" }],
|
||||
},
|
||||
manifest: '/manifest.json',
|
||||
manifest: "/manifest.json",
|
||||
verification: {
|
||||
google: 'your-google-site-verification-code',
|
||||
google: "your-google-site-verification-code",
|
||||
},
|
||||
category: 'technology',
|
||||
category: "technology",
|
||||
other: {
|
||||
'twitter:site': '@mainasara',
|
||||
'twitter:creator': '@mainasara',
|
||||
"twitter:site": "@neutrino2211",
|
||||
"twitter:creator": "@neutrino2211",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,15 +4,18 @@ import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Home",
|
||||
description: "Welcome to my personal portfolio. I'm Mainasara Tsowa, a developer and cybersecurity expert passionate about building secure, elegant digital solutions.",
|
||||
description:
|
||||
"Welcome to my personal portfolio. I'm Mainasara Tsowa, a developer and cybersecurity expert passionate about building secure, elegant digital solutions.",
|
||||
openGraph: {
|
||||
title: "Home | Mainasara Tsowa",
|
||||
description: "Welcome to my personal portfolio. Developer and cybersecurity expert building secure, elegant digital solutions.",
|
||||
description:
|
||||
"Welcome to my personal portfolio. Developer and cybersecurity expert building secure, elegant digital solutions.",
|
||||
url: "https://mainasara.dev",
|
||||
},
|
||||
twitter: {
|
||||
title: "Home | Mainasara Tsowa",
|
||||
description: "Welcome to my personal portfolio. Developer and cybersecurity expert building secure, elegant digital solutions.",
|
||||
description:
|
||||
"Welcome to my personal portfolio. Developer and cybersecurity expert building secure, elegant digital solutions.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,50 +47,14 @@ export default function Home() {
|
||||
<nav className="mb-16">
|
||||
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||
<li>
|
||||
<a
|
||||
href="#about"
|
||||
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#expertise"
|
||||
className="text-black font-bold hover:bg-teal-200 px-4 py-2 transition-colors border-2 border-black hover:border-teal-400"
|
||||
>
|
||||
Expertise
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#projects"
|
||||
className="text-black font-bold hover:bg-blue-200 px-4 py-2 transition-colors border-2 border-black hover:border-blue-400"
|
||||
>
|
||||
Projects
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#contact"
|
||||
className="text-black font-bold hover:bg-yellow-200 px-4 py-2 transition-colors border-2 border-black hover:border-yellow-400"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal mt-6">
|
||||
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||
<li>
|
||||
{/*<li>
|
||||
<a
|
||||
href="https://blog.mainasara.dev"
|
||||
className="text-black font-bold hover:bg-slate-200 px-4 py-2 transition-colors border-2 border-black hover:border-slate-400"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
</li>*/}
|
||||
<li>
|
||||
<a
|
||||
href="/digital-art"
|
||||
@@ -201,7 +168,7 @@ export default function Home() {
|
||||
Digital Art
|
||||
</h3>
|
||||
<p className="text-gray-800 text-sm">
|
||||
Exploring creativity through digital canvases
|
||||
Someone messing around with colors and shapes.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
@@ -213,7 +180,7 @@ export default function Home() {
|
||||
Mini Stories
|
||||
</h3>
|
||||
<p className="text-gray-800 text-sm">
|
||||
Capturing moments in brief narratives
|
||||
I like good narratives, so I try to imitate them.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
@@ -225,7 +192,7 @@ export default function Home() {
|
||||
Thoughts
|
||||
</h3>
|
||||
<p className="text-gray-800 text-sm">
|
||||
Reflections on technology and life
|
||||
Rants on technology, maybe something else.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
156
src/app/stories/[slug]/page.tsx
Normal file
156
src/app/stories/[slug]/page.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { loadStories } from '@/utils/mdxLoader';
|
||||
import { formatReadingTime } from '@/utils/readingTime';
|
||||
|
||||
interface StoryPageProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const storiesDirectory = process.cwd() + '/src/content/stories';
|
||||
|
||||
try {
|
||||
const filenames = fs.readdirSync(storiesDirectory);
|
||||
return filenames.map((filename: string) => ({
|
||||
slug: filename.replace(/\.mdx$/, ''),
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: StoryPageProps) {
|
||||
const story = await getStoryBySlug(params.slug);
|
||||
|
||||
if (!story) {
|
||||
return {
|
||||
title: 'Story Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${story.title} | Mainasara Tsowa`,
|
||||
description: story.excerpt,
|
||||
openGraph: {
|
||||
title: `${story.title} | Mainasara Tsowa`,
|
||||
description: story.excerpt,
|
||||
url: `https://mainasara.dev/stories/${params.slug}`,
|
||||
},
|
||||
twitter: {
|
||||
title: `${story.title} | Mainasara Tsowa`,
|
||||
description: story.excerpt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getStoryBySlug(slug: string) {
|
||||
const stories = await loadStories();
|
||||
return stories.find(story => story.id === slug.replace(/[^a-zA-Z0-9]/g, '-'));
|
||||
}
|
||||
|
||||
export default async function StoryPage({ params }: StoryPageProps) {
|
||||
const story = await getStoryBySlug(params.slug);
|
||||
|
||||
if (!story) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-yellow-50 via-orange-50 to-red-50 font-mono">
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<header className="text-center mb-16">
|
||||
<div className="mb-8">
|
||||
<div className="w-32 h-32 mx-auto bg-black rounded-full flex items-center justify-center border-4 border-yellow-300">
|
||||
<div className="text-4xl font-bold text-yellow-300">📝</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 uppercase tracking-wide">
|
||||
Mini Stories
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700 max-w-2xl mx-auto leading-relaxed font-semibold">
|
||||
Capturing moments in brief narratives
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav className="mb-16">
|
||||
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/digital-art"
|
||||
className="text-black font-bold hover:bg-purple-200 px-4 py-2 transition-colors border-2 border-black hover:border-purple-400"
|
||||
>
|
||||
Digital Art
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/stories"
|
||||
className="border-yellow-200 bg-yellow-300 px-4 py-2 border-2 text-black"
|
||||
>
|
||||
All Stories
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/thoughts"
|
||||
className="text-black font-bold hover:bg-teal-200 px-4 py-2 transition-colors border-2 border-black hover:border-teal-400"
|
||||
>
|
||||
Thoughts
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<article className="bg-white border-4 border-black rounded-none p-8 shadow-brutal">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 uppercase mb-4">
|
||||
{story.title}
|
||||
</h1>
|
||||
|
||||
<div className="flex gap-4 text-sm mb-6">
|
||||
<span className="bg-yellow-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{formatReadingTime(story.readTime)} read
|
||||
</span>
|
||||
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{new Date(story.createdAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<div className="text-gray-800 leading-relaxed whitespace-pre-line">
|
||||
{story.content}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer className="mt-20 text-center">
|
||||
<div className="bg-black text-white p-4 font-bold">
|
||||
<p className="text-sm uppercase">Built with brutalist pastels</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
src/app/thoughts/[slug]/page.tsx
Normal file
159
src/app/thoughts/[slug]/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { loadThoughts } from '@/utils/mdxLoader';
|
||||
import { formatReadingTime } from '@/utils/readingTime';
|
||||
|
||||
interface ThoughtPageProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const thoughtsDirectory = process.cwd() + '/src/content/thoughts';
|
||||
|
||||
try {
|
||||
const filenames = fs.readdirSync(thoughtsDirectory);
|
||||
return filenames.map((filename: string) => ({
|
||||
slug: filename.replace(/\.mdx$/, ''),
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ThoughtPageProps) {
|
||||
const thought = await getThoughtBySlug(params.slug);
|
||||
|
||||
if (!thought) {
|
||||
return {
|
||||
title: 'Thought Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${thought.title} | Mainasara Tsowa`,
|
||||
description: thought.excerpt,
|
||||
openGraph: {
|
||||
title: `${thought.title} | Mainasara Tsowa`,
|
||||
description: thought.excerpt,
|
||||
url: `https://mainasara.dev/thoughts/${params.slug}`,
|
||||
},
|
||||
twitter: {
|
||||
title: `${thought.title} | Mainasara Tsowa`,
|
||||
description: thought.excerpt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getThoughtBySlug(slug: string) {
|
||||
const thoughts = await loadThoughts();
|
||||
return thoughts.find(thought => thought.id === slug.replace(/[^a-zA-Z0-9]/g, '-'));
|
||||
}
|
||||
|
||||
export default async function ThoughtPage({ params }: ThoughtPageProps) {
|
||||
const thought = await getThoughtBySlug(params.slug);
|
||||
|
||||
if (!thought) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-teal-50 via-green-50 to-blue-50 font-mono">
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<header className="text-center mb-16">
|
||||
<div className="mb-8">
|
||||
<div className="w-32 h-32 mx-auto bg-black rounded-full flex items-center justify-center border-4 border-teal-300">
|
||||
<div className="text-4xl font-bold text-teal-300">🌸</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 uppercase tracking-wide">
|
||||
Thoughts
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700 max-w-2xl mx-auto leading-relaxed font-semibold">
|
||||
Reflections on technology and life
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav className="mb-16">
|
||||
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/digital-art"
|
||||
className="text-black font-bold hover:bg-purple-200 px-4 py-2 transition-colors border-2 border-black hover:border-purple-400"
|
||||
>
|
||||
Digital Art
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/stories"
|
||||
className="text-black font-bold hover:bg-yellow-200 px-4 py-2 transition-colors border-2 border-black hover:border-yellow-400"
|
||||
>
|
||||
Stories
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/thoughts"
|
||||
className="border-teal-200 bg-teal-300 px-4 py-2 border-2 text-black"
|
||||
>
|
||||
All Thoughts
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<article className="bg-white border-4 border-black rounded-none p-8 shadow-brutal">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 uppercase mb-4">
|
||||
{thought.title}
|
||||
</h1>
|
||||
|
||||
<div className="flex gap-4 text-sm mb-6">
|
||||
<span className="bg-teal-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{thought.category}
|
||||
</span>
|
||||
<span className="bg-teal-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{formatReadingTime(thought.readTime)} read
|
||||
</span>
|
||||
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{new Date(thought.createdAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<div className="text-gray-800 leading-relaxed whitespace-pre-line">
|
||||
{thought.content}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer className="mt-20 text-center">
|
||||
<div className="bg-black text-white p-4 font-bold">
|
||||
<p className="text-sm uppercase">Built with brutalist pastels</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
157
src/components/Art.tsx
Normal file
157
src/components/Art.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { artItems } from "@/data/artData";
|
||||
import { useState } from "react";
|
||||
import ArtModal from "@/components/ArtModal";
|
||||
|
||||
export default function DigitalArt() {
|
||||
const [selectedArt, setSelectedArt] = useState<(typeof artItems)[0] | null>(
|
||||
null,
|
||||
);
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleArtClick = (artItem: (typeof artItems)[0]) => {
|
||||
setSelectedArt(artItem);
|
||||
setCurrentImageIndex(0);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedArt(null);
|
||||
setCurrentImageIndex(0);
|
||||
};
|
||||
|
||||
const handleImageChange = (index: number) => {
|
||||
setCurrentImageIndex(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-pink-50 to-yellow-50 font-mono">
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<header className="text-center mb-16">
|
||||
<div className="mb-8">
|
||||
<div className="w-32 h-32 mx-auto bg-black rounded-full flex items-center justify-center border-4 border-purple-300">
|
||||
<div className="text-4xl font-bold text-purple-300">🎨</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 uppercase tracking-wide">
|
||||
Digital Art
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700 max-w-2xl mx-auto leading-relaxed font-semibold">
|
||||
Exploring creativity through digital canvases and new media
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav className="mb-16">
|
||||
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/digital-art"
|
||||
className="border-purple-200 bg-purple-300 px-4 py-2 border-2 text-black"
|
||||
>
|
||||
Digital Art
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/stories"
|
||||
className="text-black font-bold hover:bg-yellow-200 px-4 py-2 transition-colors border-2 border-black hover:border-yellow-400"
|
||||
>
|
||||
Stories
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/thoughts"
|
||||
className="text-black font-bold hover:bg-teal-200 px-4 py-2 transition-colors border-2 border-black hover:border-teal-400"
|
||||
>
|
||||
Thoughts
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="space-y-8">
|
||||
{artItems.map((art) => (
|
||||
<article
|
||||
key={art.id}
|
||||
className="bg-white border-4 border-black rounded-none p-8 shadow-brutal cursor-pointer hover:shadow-brutal-hover transition-shadow"
|
||||
onClick={() => handleArtClick(art)}
|
||||
>
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className="md:w-1/3">
|
||||
<div className="bg-purple-100 border-2 border-black rounded-none w-full h-48 flex items-center justify-center">
|
||||
<img
|
||||
src={
|
||||
art.type === "collection"
|
||||
? art.images![0].url
|
||||
: art.imageUrl
|
||||
}
|
||||
alt={art.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4 uppercase">
|
||||
{art.title}
|
||||
</h2>
|
||||
<p className="text-gray-800 mb-4 leading-relaxed">
|
||||
{art.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<span className="bg-purple-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{art.medium}
|
||||
</span>
|
||||
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{art.type === "collection" ? "Collection" : "Single"}
|
||||
</span>
|
||||
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{new Date(art.createdAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
{art.type === "collection" && art.images && (
|
||||
<span className="bg-blue-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{art.images.length} images
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</main>
|
||||
|
||||
<footer className="mt-20 text-center">
|
||||
<div className="bg-black text-white p-4 font-bold">
|
||||
<p className="text-sm uppercase">Built with brutalist pastels</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<ArtModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
artItem={selectedArt}
|
||||
currentImageIndex={currentImageIndex}
|
||||
onImageChange={handleImageChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
160
src/components/ArtModal.tsx
Normal file
160
src/components/ArtModal.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ArtItem } from "@/data/creative";
|
||||
|
||||
interface ArtModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
artItem: ArtItem | null;
|
||||
currentImageIndex: number;
|
||||
onImageChange: (index: number) => void;
|
||||
}
|
||||
|
||||
export default function ArtModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
artItem,
|
||||
currentImageIndex,
|
||||
onImageChange,
|
||||
}: ArtModalProps) {
|
||||
if (!isOpen || !artItem) return null;
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevImage = () => {
|
||||
if (artItem.images && artItem.images.length > 1) {
|
||||
const newIndex =
|
||||
currentImageIndex > 0
|
||||
? currentImageIndex - 1
|
||||
: artItem.images.length - 1;
|
||||
onImageChange(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextImage = () => {
|
||||
if (artItem.images && artItem.images.length > 1) {
|
||||
const newIndex =
|
||||
currentImageIndex < artItem.images.length - 1
|
||||
? currentImageIndex + 1
|
||||
: 0;
|
||||
onImageChange(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const currentImage = artItem.images?.[currentImageIndex];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2 uppercase">
|
||||
{artItem.title}
|
||||
</h2>
|
||||
<p className="text-gray-700 mb-4">{artItem.description}</p>
|
||||
<div className="flex gap-4 text-sm">
|
||||
<span className="text-purple-200 px-3 py-1 border-2 border-black font-bold">
|
||||
{artItem.medium}
|
||||
</span>
|
||||
<span className="text-gray-200 px-3 py-1 border-2 border-black font-bold">
|
||||
{artItem.type === "collection" ? "Collection" : "Single"}
|
||||
</span>
|
||||
<span className="text-gray-200 px-3 py-1 border-2 border-black font-bold">
|
||||
{new Date(artItem.createdAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-2xl font-bold text-white w-10 h-10 flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentImage && (
|
||||
<div className="relative">
|
||||
<div className="w-full h-96 flex items-center justify-center mb-4">
|
||||
<img
|
||||
src={currentImage.url}
|
||||
alt={currentImage.title}
|
||||
className="max-w-full max-h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{artItem.type === "collection" &&
|
||||
artItem.images &&
|
||||
artItem.images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={handlePrevImage}
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 px-3 py-1 font-bold"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNextImage}
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 px-3 py-1 font-bold"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
{currentImage.title || `Image ${currentImageIndex + 1}`}
|
||||
</h3>
|
||||
{currentImage.description && (
|
||||
<p className="text-gray-700">{currentImage.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{artItem.type === "collection" &&
|
||||
artItem.images &&
|
||||
artItem.images.length > 1 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="text-lg font-bold mb-3 text-center">
|
||||
Collection Preview
|
||||
</h4>
|
||||
<div className="flex gap-2 overflow-x-auto pb-2 justify-center">
|
||||
{artItem.images.map((image, index) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => onImageChange(index)}
|
||||
className={`flex-shrink-0 w-20 h-20 border-2 ${
|
||||
index === currentImageIndex
|
||||
? "border-purple-500"
|
||||
: "border-black"
|
||||
} rounded-none flex items-center justify-center`}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.title}
|
||||
className="max-w-full max-h-full"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Story } from '@/data/creative';
|
||||
import { formatReadingTime } from '@/utils/readingTime';
|
||||
|
||||
@@ -29,6 +30,12 @@ export default function ExpandableStory({ story }: ExpandableStoryProps) {
|
||||
<p className="text-gray-600 italic mb-4">{story.excerpt}</p>
|
||||
|
||||
<div className="flex gap-4 text-sm">
|
||||
<Link
|
||||
href={`/stories/${story.id}`}
|
||||
className="bg-yellow-200 text-black px-3 py-1 border-2 border-black font-bold hover:bg-yellow-300 transition-colors"
|
||||
>
|
||||
Read Full Story
|
||||
</Link>
|
||||
<span className="bg-yellow-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||
{formatReadingTime(story.readTime)} read
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Thought } from "@/data/creative";
|
||||
import { formatReadingTime } from "@/utils/readingTime";
|
||||
import Markdown from "react-markdown";
|
||||
@@ -36,6 +37,12 @@ export default function ExpandableThought({ thought }: ExpandableThoughtProps) {
|
||||
<p className="text-gray-600 italic mb-4">{thought.excerpt}</p>
|
||||
|
||||
<div className="flex gap-4 text-sm">
|
||||
<Link
|
||||
href={`/thoughts/${thought.id}`}
|
||||
className="bg-teal-200 text-black px-3 py-1 border-2 border-black font-bold hover:bg-teal-300 transition-colors"
|
||||
>
|
||||
Read Full Thought
|
||||
</Link>
|
||||
<span
|
||||
className={`${categoryColors[thought.category]} text-black px-3 py-1 border-2 border-black font-bold`}
|
||||
>
|
||||
|
||||
45
src/data/artData.ts
Normal file
45
src/data/artData.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ArtItem } from "./creative";
|
||||
|
||||
export const artItems: ArtItem[] = [
|
||||
{
|
||||
id: "screen-saver",
|
||||
title: "Glassy Screen Saver",
|
||||
description:
|
||||
"Have a set of beautifully translucent glass shards accompany you through the day.",
|
||||
createdAt: "2023-01-16",
|
||||
medium: "Digital Abstract Art",
|
||||
type: "collection",
|
||||
images: [
|
||||
{
|
||||
id: "artboard-2",
|
||||
url: "/media/art/screensaver/Artboard – 2.png",
|
||||
title: "Flow",
|
||||
description: "A calm set of fluttery glass shards",
|
||||
},
|
||||
{
|
||||
id: "artboard-4",
|
||||
url: "/media/art/screensaver/Artboard – 4.png",
|
||||
title: "It Cuts",
|
||||
description: "Sharp, daring and a clear warning.",
|
||||
},
|
||||
{
|
||||
id: "screensaver-1",
|
||||
url: "/media/art/screensaver/ScreenSaver-1.png",
|
||||
title: "Unoffensive",
|
||||
description: "Yes, it can cut. But it does not want to.",
|
||||
},
|
||||
{
|
||||
id: "screensaver-2",
|
||||
url: "/media/art/screensaver/ScreenSaver-2.png",
|
||||
title: "Rebel",
|
||||
description: "These shards are not afraid to scare you.",
|
||||
},
|
||||
{
|
||||
id: "screensaver-3",
|
||||
url: "/media/art/screensaver/ScreenSaver-3.png",
|
||||
title: "Stained",
|
||||
description: "What if our calm friend from earlier was stained?",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -5,6 +5,15 @@ export interface ArtItem {
|
||||
imageUrl?: string;
|
||||
createdAt: string;
|
||||
medium: string;
|
||||
type: 'collection' | 'single';
|
||||
images?: ArtImage[];
|
||||
}
|
||||
|
||||
export interface ArtImage {
|
||||
id: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Story {
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
import { ArtItem, Story, Thought } from './creative';
|
||||
import { loadStories, loadThoughts } from '@/utils/mdxLoader';
|
||||
|
||||
export const artItems: ArtItem[] = [
|
||||
{
|
||||
id: 'digital-landscapes',
|
||||
title: 'Digital Landscapes',
|
||||
description: 'Exploring the intersection of nature and technology through vibrant digital canvases',
|
||||
createdAt: '2024-01-15',
|
||||
medium: 'Digital Painting'
|
||||
},
|
||||
{
|
||||
id: 'abstract-emotions',
|
||||
title: 'Abstract Emotions',
|
||||
description: 'A series exploring human emotions through color and form',
|
||||
createdAt: '2024-02-20',
|
||||
medium: 'Mixed Media'
|
||||
},
|
||||
{
|
||||
id: 'cyberpunk-dreams',
|
||||
title: 'Cyberpunk Dreams',
|
||||
description: 'Futuristic cityscapes blending retro aesthetics with modern technology',
|
||||
createdAt: '2024-03-10',
|
||||
medium: '3D Render'
|
||||
}
|
||||
];
|
||||
import { Story, Thought } from "./creative";
|
||||
import { loadStories, loadThoughts } from "@/utils/mdxLoader";
|
||||
|
||||
// Load stories from MDX files
|
||||
export async function getStories(): Promise<Story[]> {
|
||||
|
||||
@@ -2,42 +2,18 @@ export interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: 'security' | 'development' | 'art' | 'other';
|
||||
category: "security" | "development" | "art" | "other";
|
||||
link?: string;
|
||||
featured: boolean;
|
||||
}
|
||||
|
||||
export const projects: Project[] = [
|
||||
{
|
||||
id: 'security-scanner',
|
||||
title: 'Security Scanner',
|
||||
description: 'Automated vulnerability detection tool',
|
||||
category: 'security',
|
||||
link: '#',
|
||||
featured: true
|
||||
id: "valradar",
|
||||
title: "Valradar",
|
||||
description: "OSINT and general purpose multiprocessing framework.",
|
||||
category: "security",
|
||||
link: "https://github.com/neutrino2211/valradar",
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
id: 'art-portfolio',
|
||||
title: 'Art Portfolio',
|
||||
description: 'Digital art showcase platform',
|
||||
category: 'art',
|
||||
link: '#',
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 'api-gateway',
|
||||
title: 'API Gateway',
|
||||
description: 'Secure microservices gateway with authentication',
|
||||
category: 'development',
|
||||
link: '#',
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 'threat-detector',
|
||||
title: 'Threat Detector',
|
||||
description: 'Machine learning-based threat detection system',
|
||||
category: 'security',
|
||||
link: '#',
|
||||
featured: false
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user