Main pages

This commit is contained in:
2026-03-10 23:45:14 +01:00 Unverified
parent 2b4897ad65
commit b6a78abae8
17 changed files with 2244 additions and 116 deletions
+68
View File
@@ -0,0 +1,68 @@
// app/components/BlurredCode.tsx
'use client';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
const pythonCode = `import quantum_entanglement
import neural_net_vibes
from cosmos import dark_matter
class HotdogAnalyzer(quantum_entanglement.Observer):
def __init__(self, mustard_ratio=0.8):
self.mustard_ratio = mustard_ratio
self.is_sandwich = None
self.__secret_sauce = "01101000 01101101 01101101"
async def _calculate_bun_topology(self, bread_manifold):
"""Uses string theory to determine if a bun is contiguous."""
try:
dimensions = await dark_matter.measure(bread_manifold)
if dimensions > 3:
raise Exception("Bun exists in the 4th dimension.")
return dimensions * self.mustard_ratio
except OverflowError:
return "Just eat it already"
@neural_net_vibes.optimize(epochs=42069)
def check_is_sandwich(self, hotdog):
if hotdog.is_taco():
self.is_sandwich = False
return self.is_sandwich
# Initiate brute-force topological check
for atom in hotdog.get_atoms():
if atom.vibe_check() == "sus":
hotdog.add_ketchup(override=True)
self.is_sandwich = True if hotdog.entropy < 9000 else False
return "Maybe?"
if __name__ == "__main__":
analyzer = HotdogAnalyzer(mustard_ratio=1.1)
analyzer.check_is_sandwich(target="glizzy")`;
export default function BlurredCode() {
return (
<div className="absolute inset-0 overflow-hidden bg-[#0d1117] z-0 pointer-events-none flex justify-center">
<div className="relative w-full h-full text-left p-8 opacity-40 blur-[2px] group-hover:blur-none group-hover:opacity-100 transition-all duration-700 animate-[scrollUp_40s_linear_infinite]">
<SyntaxHighlighter
language="python"
style={vscDarkPlus}
customStyle={{ background: 'transparent', margin: 0, padding: 0, fontSize: '14px' }}
>
{pythonCode + '\n\n\n' + pythonCode + '\n\n\n' + pythonCode}
</SyntaxHighlighter>
</div>
<style dangerouslySetInnerHTML={{
__html: `
@keyframes scrollUp {
0% { transform: translateY(0); }
100% { transform: translateY(-33.33%); }
}
`}} />
</div>
);
}
+89
View File
@@ -0,0 +1,89 @@
// app/components/MatrixRain.tsx
'use client';
import { useEffect, useRef } from 'react';
interface MatrixRainProps {
color?: string;
fps?: number;
}
export default function MatrixRain({ color = '#84c0a0', fps = 15 }: MatrixRainProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const lastWidth = useRef<number>(0);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let intervalId: NodeJS.Timeout;
const fontSize = 16;
let columns = 0;
let drops: number[] = [];
const setCanvasSize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight + 120;
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const initDrops = () => {
columns = Math.floor(canvas.width / fontSize);
drops = [];
for (let x = 0; x < columns; x++) {
drops[x] = Math.random() * -100;
}
};
lastWidth.current = window.innerWidth;
setCanvasSize();
initDrops();
const draw = () => {
ctx.fillStyle = 'rgba(13, 17, 23, 0.2)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = `bold ${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
const text = Math.random() > 0.5 ? '0' : '1';
ctx.fillStyle = color;
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
}
};
intervalId = setInterval(draw, 1000 / fps);
const handleResize = () => {
if (window.innerWidth !== lastWidth.current) {
lastWidth.current = window.innerWidth;
setCanvasSize();
initDrops();
}
};
window.addEventListener('resize', handleResize);
return () => {
clearInterval(intervalId);
window.removeEventListener('resize', handleResize);
};
}, [color, fps]);
return (
<div className="fixed inset-0 pointer-events-none w-screen h-[110vh] bg-[#0d1117]" style={{ zIndex: -1 }}>
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full opacity-[0.15] blur-[0.5px]"
/>
</div>
);
}
+68
View File
@@ -0,0 +1,68 @@
// app/components/Navbar.tsx
'use client';
import Link from 'next/link';
import { useState } from 'react';
export default function Navbar() {
const [isExpanded, setIsExpanded] = useState(false);
const toggleMenu = () => setIsExpanded(!isExpanded);
const closeMenu = () => setIsExpanded(false);
return (
<header className="fixed h-25 bg-[#0d1117]/80 backdrop-blur-md border-b border-white/10 left-0 right-0 z-100 transition-all duration-300">
<nav className="flex justify-between h-full items-center max-w-400 px-8 mx-auto">
<Link href="/" className="group text-white no-underline flex uppercase font-extrabold items-center tracking-[1px] transition-transform hover:scale-105" onClick={closeMenu}>
<div className="w-12 h-12 flex justify-center items-center rounded-full bg-[#84c0a0]/10 border border-[#84c0a0]/30 shadow-[0_0_15px_rgba(132,192,160,0.2)] group-hover:bg-[#84c0a0]/20 transition-all duration-300">
<img src="/assets/logos/logo.svg" alt="Wiking" className="w-8 h-8" />
</div>
<span className="hidden md:block ml-4 text-sm tracking-widest text-[#84c0a0] font-mono group-hover:text-white transition-colors">
SODERBERG TECH
</span>
</Link>
<button
type="button"
className="cursor-pointer border-none bg-transparent w-10 h-10 flex items-center justify-center flex-col md:hidden relative group"
aria-expanded={isExpanded}
onClick={toggleMenu}
>
<span className={`block h-0.5 bg-white transition-all duration-300 ease-in-out group-hover:bg-[#84c0a0] ${isExpanded ? 'absolute m-0 w-7 rotate-45' : 'w-6 m-0.75'}`}></span>
<span className={`block h-0.5 bg-white transition-all duration-300 ease-in-out group-hover:bg-[#84c0a0] ${isExpanded ? 'opacity-0 w-0' : 'w-6 m-0.75'}`}></span>
<span className={`block h-0.5 bg-white transition-all duration-300 ease-in-out group-hover:bg-[#84c0a0] ${isExpanded ? 'absolute m-0 w-7 -rotate-45' : 'w-6 m-0.75'}`}></span>
</button>
<div
className={`fixed top-25 bottom-0 left-0 right-0 transition-all duration-300 md:static md:block md:opacity-100 md:visible md:h-full ${isExpanded ? 'bg-[#0d1117]/60 backdrop-blur-sm opacity-100 visible' : 'opacity-0 invisible'}`}
onClick={closeMenu}
>
<ul
className={`list-none absolute flex flex-col items-center left-0 right-0 m-4 rounded-3xl bg-[#131920]/70 backdrop-blur-2xl md:backdrop-blur-none border border-white/10 shadow-2xl md:m-0 md:p-0 md:border-none md:shadow-none md:static md:flex-row md:w-full md:h-full md:bg-transparent md:justify-end gap-2 md:gap-8 transition-all duration-300 ${isExpanded ? 'p-8 translate-y-0 opacity-100' : '-translate-y-4 opacity-0 md:translate-y-0 md:opacity-100'}`}
onClick={(e) => e.stopPropagation()}
>
{[
{ name: 'Home', path: '/' },
{ name: 'Gallery', path: '/gallery' },
{ name: 'Projects', path: '/projects' },
{ name: 'Contact', path: '/contact' },
].map((link) => (
<li key={link.name} className="w-full md:w-auto">
<Link
className="relative flex justify-center items-center text-gray-300 uppercase font-bold tracking-[2px] text-sm px-4 py-3 rounded-xl hover:text-white hover:bg-white/5 transition-all duration-300 group overflow-hidden"
href={link.path}
onClick={closeMenu}
>
<span className="relative z-10">{link.name}</span>
<span className="absolute bottom-0 left-1/2 w-0 h-0.5 bg-[#84c0a0] transition-all duration-300 group-hover:w-1/2 group-hover:-translate-x-1/2 shadow-[0_0_10px_rgba(132,192,160,0.8)]"></span>
</Link>
</li>
))}
</ul>
</div>
</nav>
</header>
);
}
+64
View File
@@ -0,0 +1,64 @@
// app/components/PhotoGallery.tsx
'use client';
import { Gallery, Item } from 'react-photoswipe-gallery';
import 'photoswipe/dist/photoswipe.css';
import Image from 'next/image';
interface GalleryImage {
src: string;
width: number;
height: number;
name: string;
}
export default function PhotoGallery({ images }: { images: GalleryImage[] }) {
return (
<div className="bg-[#0a0d11] flex justify-center w-full min-h-screen border-t border-white/5">
<Gallery>
<div className="grid grid-cols-2 lg:grid-cols-4 2xl:grid-cols-6 grid-flow-dense w-full">
{images.map((img, index) => {
const isHorizontal = img.width > img.height;
return (
<div
key={index}
className={`relative bg-[#0d1117] overflow-hidden group cursor-pointer border-[0.5px] border-white/5 ${isHorizontal ? 'col-span-2' : 'col-span-1'}`}
>
<Item
original={img.src}
thumbnail={img.src}
width={img.width}
height={img.height}
alt={`Gallery image ${img.name}`}
>
{({ ref, open }) => (
<div
ref={ref as React.RefCallback<HTMLDivElement>}
onClick={open}
className="w-full h-full relative block min-h-75"
>
<div className="absolute inset-0 border-8 border-[#0d1117]/50 transition-all duration-300 z-10 pointer-events-none"></div>
<div className="absolute inset-4 border-2 border-white/10 transition-all duration-500 z-10 pointer-events-none group-hover:scale-95 group-hover:border-[#84c0a0]/60 [clip-path:polygon(0_calc(100%-1rem),0_100%,1rem_100%,1rem_0,0_0,0_1rem,100%_1rem,100%_0,calc(100%-1rem)_0,calc(100%-1rem)_100%,100%_100%,100%_calc(100%-1rem))]"></div>
<Image
src={img.src}
alt={`Photo of ${img.name.replace('.jpg', '')}`}
width={isHorizontal ? 1200 : 800}
height={isHorizontal ? 800 : 800}
quality={90}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="w-full h-full object-cover block opacity-80 transition-all duration-700 group-hover:scale-110 group-hover:opacity-100"
/>
</div>
)}
</Item>
</div>
);
})}
</div>
</Gallery>
</div>
);
}
+101
View File
@@ -0,0 +1,101 @@
// app/contact/page.tsx
import { FaInstagram, FaLinkedin, FaDiscord, FaEnvelope, FaMapMarkerAlt } from 'react-icons/fa';
export default function ContactPage() {
return (
<main className="flex flex-col flex-1 relative bg-[#0d1117] overflow-hidden">
{/* 1. Immersive Mountain Background */}
<img
src="/assets/images/snowy_mountain_peaks.jpg"
alt="Snowy Mountains"
className="absolute inset-0 w-full h-full object-cover z-0"
/>
{/* Dark blur overlay so the text and cards remain perfectly readable */}
<div className="absolute inset-0 bg-[#0d1117]/85 backdrop-blur-md z-0"></div>
<div className="h-25 shrink-0 z-10"></div>
<section className="relative z-10 flex-1 flex flex-col justify-center items-center py-12 px-6">
<div className="mb-12 text-center">
<h1 className="text-white text-[40px] md:text-[55px] uppercase font-black tracking-tighter drop-shadow-lg">
Find me here!
</h1>
<p className="text-[#84c0a0] font-mono font-bold tracking-widest mt-2 uppercase">
Transmission & Socials
</p>
</div>
<div className="w-full max-w-3xl bg-white/5 backdrop-blur-xl rounded-[32px] border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)] p-8 md:p-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="flex flex-col gap-8">
<h3 className="text-white text-xl font-bold border-b border-white/10 pb-4 uppercase tracking-wider">Direct Info</h3>
<div className="flex items-center gap-4 group">
<div className="w-12 h-12 rounded-2xl bg-[#84c0a0]/10 flex items-center justify-center border border-[#84c0a0]/30 group-hover:bg-[#84c0a0]/20 transition-all">
<FaEnvelope className="text-[#84c0a0] text-xl" />
</div>
<div>
<p className="text-gray-400 text-xs uppercase font-bold tracking-widest">Email</p>
<a href="mailto:support@soderberg.tech" className="text-white font-medium hover:text-[#84c0a0] transition-colors break-all">
support@soderberg.tech
</a>
</div>
</div>
<div className="flex items-center gap-4 group">
<div className="w-12 h-12 rounded-2xl bg-[#84c0a0]/10 flex items-center justify-center border border-[#84c0a0]/30">
<FaMapMarkerAlt className="text-[#84c0a0] text-xl" />
</div>
<div>
<p className="text-gray-400 text-xs uppercase font-bold tracking-widest">Location</p>
<p className="text-white font-medium">Höganäs, Sweden</p>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<h3 className="text-white text-xl font-bold border-b border-white/10 pb-4 uppercase tracking-wider">Networks</h3>
<div className="grid grid-cols-1 gap-3">
<a href="https://www.linkedin.com/in/william-s%C3%B6derberg-5b79bb247" target="_blank" className="flex items-center justify-between p-4 rounded-2xl bg-white/5 border border-white/5 hover:border-[#0077b5]/50 hover:bg-[#0077b5]/10 transition-all group">
<div className="flex items-center gap-3 text-white font-bold">
<FaLinkedin className="text-xl text-[#0077b5]" /> LinkedIn
</div>
<span className="text-gray-500 group-hover:text-[#0077b5] group-hover:translate-x-1 transition-all"></span>
</a>
<a href="https://discordapp.com/users/271290165106966528" target="_blank" className="flex items-center justify-between p-4 rounded-2xl bg-white/5 border border-white/5 hover:border-[#5865F2]/50 hover:bg-[#5865F2]/10 transition-all group">
<div className="flex items-center gap-3 text-white font-bold">
<FaDiscord className="text-xl text-[#5865F2]" /> Discord
</div>
<span className="text-gray-500 group-hover:text-[#5865F2] group-hover:translate-x-1 transition-all"></span>
</a>
<a href="https://www.instagram.com/will.i.am.soderberg/" target="_blank" className="flex items-center justify-between p-4 rounded-2xl bg-white/5 border border-white/5 hover:border-[#E1306C]/50 hover:bg-[#E1306C]/10 transition-all group">
<div className="flex items-center gap-3 text-white font-bold">
<FaInstagram className="text-xl text-[#E1306C]" /> Instagram
</div>
<span className="text-gray-500 group-hover:text-[#E1306C] group-hover:translate-x-1 transition-all"></span>
</a>
</div>
</div>
</div>
<div className="mt-12">
<a
href="mailto:support@soderberg.tech?subject=Contact%20me"
className="w-full flex items-center justify-center gap-3 py-5 rounded-2xl bg-[#84c0a0] text-[#0d1117] font-bold text-lg hover:scale-[1.02] active:scale-[0.98] transition-all shadow-[0_0_20px_rgba(132,192,160,0.3)]"
>
<FaEnvelope /> Send an Email
</a>
</div>
</div>
</section>
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
// app/css.d.ts
declare module '*.css';
+67
View File
@@ -0,0 +1,67 @@
// app/gallery/page.tsx
import fs from 'fs';
import path from 'path';
import sizeOf from 'image-size';
import PhotoGallery from '@/app/components/PhotoGallery';
async function getImages() {
const galleryDir = path.join(process.cwd(), 'public/gallery');
const files = fs.readdirSync(galleryDir);
const images = files
.filter((file) => file.endsWith('.jpg') || file.endsWith('.png') || file.endsWith('.jpeg'))
.map((file) => {
const filePath = path.join(galleryDir, file);
const buffer = fs.readFileSync(filePath);
const dimensions = sizeOf(buffer);
return {
src: `/gallery/${file}`,
width: dimensions.width || 1000,
height: dimensions.height || 1000,
name: file,
};
});
return images;
}
export default async function GalleryPage() {
const images = await getImages();
return (
<main className="flex flex-col flex-1 relative bg-[#0d1117] overflow-hidden">
{/* 1. Background Ambient Glow */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-150 h-150 bg-[#84c0a0]/10 blur-[150px] rounded-full pointer-events-none z-0"></div>
<div className="h-25 shrink-0"></div>
{/* 2. Modernized Header */}
<section className="relative z-10 flex flex-col justify-center items-center py-20 px-6">
<div className="text-center">
<h1 className="text-white text-[40px] md:text-[55px] uppercase font-black tracking-tighter drop-shadow-lg mb-4">
Visual Archives
</h1>
<div className="flex items-center justify-center gap-4 mb-6">
<div className="w-12 h-px bg-[#84c0a0]"></div>
<p className="text-[#84c0a0] font-mono font-bold tracking-widest uppercase text-sm">
Photography Collection
</p>
<div className="w-12 h-px bg-[#84c0a0]"></div>
</div>
<p className="text-gray-400 font-light max-w-2xl mx-auto text-lg leading-relaxed">
A curated collection of photographic captures. Finding the best subjects to capture on film, from natural landscapes to the night sky.
</p>
</div>
</section>
{/* 3. The Gallery Grid */}
<div className="relative z-10 w-full">
<PhotoGallery images={images} />
</div>
</main>
);
}
+20 -21
View File
@@ -1,26 +1,25 @@
/* app/globals.css */
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
@keyframes zoomIn {
0% {
opacity: 0;
transform: scale3d(.3, .3, .3);
}
100% {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@keyframes fadeIn {
0% {
opacity: 0;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
100% {
opacity: 1;
}
}
+22
View File
@@ -0,0 +1,22 @@
// app/globals.d.ts
import * as React from 'react';
declare global {
namespace JSX {
interface IntrinsicElements {
'model-viewer': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
src?: string;
poster?: string;
'ios-src'?: string;
reveal?: string;
'shadow-intensity'?: string | number;
ar?: boolean;
'camera-controls'?: boolean;
'touch-action'?: string;
alt?: string;
class?: string;
};
}
}
}
+23 -22
View File
@@ -1,34 +1,35 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
// app/layout.tsx
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
import Navbar from './components/Navbar';
import './globals.css';
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
export const metadata = {
title: 'Soderberg Tech',
description: 'William Söderberg, Technical high school engineer from Höganäs, Sweden',
};
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body className="flex flex-col min-h-screen m-0 p-0 font-sans bg-[#0d1117]">
<Navbar />
<main className="flex flex-col flex-1">
{children}
</main>
<footer className="w-full relative z-50 mt-auto border-t border-white/5">
<div className="bg-[#0d1117] flex justify-center items-center h-10">
<a className="text-[#a0aab6] no-underline flex font-medium items-center hover:text-white transition-colors text-sm tracking-widest uppercase" href="/">
© William Söderberg {new Date().getFullYear()}
</a>
</div>
</footer>
</body>
</html>
);
}
}
+217 -57
View File
@@ -1,65 +1,225 @@
import Image from "next/image";
// app/page.tsx
'use client';
import React from 'react';
import Link from 'next/link';
import MatrixRain from '@/app/components/MatrixRain';
import { FaInstagram, FaLinkedin, FaDiscord, FaEnvelope, FaCamera, FaVolleyballBall, FaTree, FaCode } from 'react-icons/fa';
import { TbCube3dSphere } from 'react-icons/tb';
export default function Home() {
const calculateAge = (birthDateString: string): number => {
const today = new Date();
const birthDate = new Date(birthDateString);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
};
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
<div className="relative w-full bg-[#0d1117] text-white overflow-hidden font-sans">
{/* 1. HERO SECTION */}
<section className="relative h-screen min-h-175 flex flex-col justify-center items-center overflow-hidden">
{/* Ambient Overlay for contrast */}
<div className="absolute inset-0 bg-linear-to-b from-transparent via-[#0d1117]/50 to-[#0d1117] z-1"></div>
<div className="relative z-10 flex flex-col items-center px-4">
<div className="bg-[#84c0a0] rounded-full h-45 w-45 md:h-55 md:w-55 flex justify-center items-center shadow-[0_0_50px_rgba(132,192,160,0.3)] animate-[zoomIn_1000ms_ease-out]">
<img src="/assets/logos/logo.svg" alt="Logo" className="h-[60%]" />
</div>
<h2 className="text-[#84c0a0] font-mono font-bold tracking-[0.3em] uppercase mt-12 text-sm md:text-xl text-center">
Technical High School Engineer
</h2>
<h1 className="text-white text-center uppercase text-[3rem] md:text-[6rem] font-black leading-none mt-4 drop-shadow-2xl">
William Söderberg
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="mt-10 flex gap-4">
<Link href="/projects" className="bg-[#84c0a0] text-[#0d1117] px-8 py-3 rounded-full font-bold uppercase tracking-wider hover:scale-105 transition-transform shadow-lg">
View Portfolios
</Link>
</div>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</section>
{/* 2. ABOUT ME SECTION */}
<section className="relative py-24 px-6">
{/* Background Glow */}
<div className="absolute top-1/2 left-0 -translate-y-1/2 w-125 h-125 bg-[#84c0a0]/5 blur-[120px] rounded-full pointer-events-none"></div>
<div className="max-w-6xl mx-auto">
{/* Subtitle Header */}
<div className="text-center mb-12">
<h2 className="text-white text-4xl md:text-5xl font-black uppercase tracking-tighter">About Me</h2>
<p className="text-[#84c0a0] mt-4 font-mono uppercase text-sm tracking-widest font-bold">Identity & Background</p>
</div>
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-[40px] overflow-hidden flex flex-col lg:flex-row shadow-2xl">
{/* Left Column: Profile with "Scanning" Effect */}
<div className="lg:w-2/5 relative group border-b lg:border-b-0 lg:border-r border-white/10">
<img
src="/assets/images/profile_picture.jpg"
alt="William Söderberg"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-linear-to-t from-[#0d1117] via-transparent to-transparent opacity-60"></div>
<div className="absolute bottom-8 left-8">
<h3 className="text-white text-2xl font-bold uppercase tracking-tight">William Söderberg</h3>
<p className="text-[#84c0a0] font-mono text-sm mt-1">EST. 2002 | SWEDEN</p>
</div>
</div>
{/* Right Column: Bio Content */}
<div className="lg:w-3/5 p-8 md:p-16 flex flex-col justify-center">
<div className="flex items-center gap-4 mb-8">
<div className="w-12 h-1 border-t-2 border-[#84c0a0]"></div>
<h2 className="text-white text-3xl font-bold uppercase tracking-widest">DATA CARD</h2>
</div>
<p className="text-gray-300 text-lg leading-relaxed font-light">
My name is William Söderberg and I'm {calculateAge("2002-07-17")} years old. I have a degree in technical high school engineering specialized in design and industrial manufacturing, with heavy influence from IT and coding. I love problem solving and finding new ways to solve the most complicated problems.
</p>
<div className="mt-10 grid grid-cols-2 gap-6 border-t border-white/10 pt-10">
<div>
<p className="text-xs font-bold text-[#84c0a0] uppercase tracking-widest mb-1">Focus</p>
<p className="text-white font-medium text-sm">Industrial Design & IT</p>
</div>
<div>
<p className="text-xs font-bold text-[#84c0a0] uppercase tracking-widest mb-1">Passions</p>
<p className="text-white font-medium text-sm">Volleyball & Computers</p>
</div>
</div>
</div>
</div>
</div>
</main>
</section>
{/* 3. WHAT DO I DO SECTION */}
<section className="relative py-24 px-6 overflow-hidden">
{/* Subtle Background Image Integration */}
<img src="/assets/images/leaves.jpg" alt="Leaves" className="absolute inset-0 w-full h-full object-cover z-0" />
<div className="absolute inset-0 bg-[#0d1117]/50 z-0"></div>
<div className="relative z-10 max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-white text-4xl md:text-5xl font-black uppercase tracking-tighter">What do I do?</h2>
<p className="text-[#84c0a0] mt-4 font-mono uppercase text-sm tracking-widest font-bold">A multidisciplinary engineer</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ icon: <FaVolleyballBall />, title: 'Volleyball', desc: 'Played for over 5 years as a middle blocker with Lerbergets and Ängelholms clubs.' },
{ icon: <FaTree />, title: 'Young Ranger', desc: 'Part of the Youth+ Network at Europarc Federation, working in management at Kullaberg.' },
{ icon: <FaCamera />, title: 'Photography', desc: 'Capturing nature and people, finding the best subjects on film.' },
{ icon: <TbCube3dSphere />, title: 'CAD', desc: 'Specialized in design and manufacturing, modeling everything from concepts to architecture.' },
{ icon: <FaCode />, title: 'Coding', desc: 'Developing automation scripts and custom programs to solve unique problems.' }
].map((item, idx) => (
<div key={idx} className="group p-8 rounded-3xl bg-white/5 backdrop-blur-md border border-white/10 hover:border-[#84c0a0]/40 hover:bg-white/10 transition-all duration-300">
<div className="text-[#84c0a0] text-4xl mb-6 w-fit group-hover:scale-110 transition-transform duration-300">{item.icon}</div>
<h3 className="text-white text-2xl font-bold mb-4">{item.title}</h3>
<p className="text-gray-400 text-sm leading-relaxed">{item.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* 4. PHOTO GALLERY SECTION */}
<section className="py-24 px-6 bg-[#0d1117]">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-white text-4xl md:text-5xl font-black uppercase tracking-tighter">Photography</h2>
<p className="text-[#84c0a0] mt-4 font-mono uppercase text-sm tracking-widest font-bold">Capturing the world</p>
</div>
<div className="flex flex-col lg:flex-row items-center lg:items-start gap-12 lg:gap-16">
{/* My Photos (Ynni Image) */}
<div className="relative w-full lg:w-1/2">
<img src="/assets/images/ynni.jpg" alt="My Photos" className="w-full rounded-4xl shadow-2xl object-cover lg:h-160" />
<div className="absolute top-[10%] -left-4 lg:-left-12 bg-white/10 backdrop-blur-xl border border-white/20 py-6 px-10 shadow-2xl rounded-2xl">
<h2 className="text-[30px] lg:text-[40px] text-white font-black whitespace-nowrap uppercase tracking-wider">My Photos</h2>
</div>
</div>
{/* Night Sky Collection */}
<div className="flex flex-col items-start w-full lg:w-1/2 lg:pt-8">
<img src="/assets/images/night_sky.jpg" alt="Night Sky" className="w-full max-w-125 rounded-3xl shadow-xl border border-white/10" />
<h3 className="text-white text-[26px] mt-8 font-bold uppercase tracking-wide">My Photography collection</h3>
<div className="w-12 h-1 border-t-2 border-[#84c0a0] my-4"></div>
<p className="text-gray-400 text-[18px] font-light leading-relaxed">
Taking photos is just one of my many hobbies. Finding the best subjects to capture on film, from nature to the night skies.
</p>
<Link href="/gallery" className="mt-8 bg-[#84c0a0] text-[#0d1117] px-8 py-3 rounded-full font-bold uppercase tracking-wider hover:scale-105 transition-transform shadow-[0_0_20px_rgba(132,192,160,0.3)]">
More photos
</Link>
</div>
</div>
{/* Singapore Full Width wrapper */}
<div className="w-full mt-32 relative h-125 md:h-175 rounded-[40px] overflow-hidden shadow-2xl border border-white/10 group">
<img src="/assets/images/singapore.jpg" alt="Singapore" className="absolute inset-0 w-full h-full object-cover transition-transform duration-2000 group-hover:scale-105" />
<div className="absolute inset-0 bg-linear-to-t from-[#0d1117]/90 via-[#0d1117]/20 to-transparent"></div>
<div className="relative z-10 w-full h-full p-8 md:p-16 flex items-end md:items-start">
<div className="bg-[#0d1117]/30 backdrop-blur-xl border border-white/10 p-8 max-w-87.5 rounded-3xl shadow-2xl">
<p className="text-[18px] font-medium text-white leading-relaxed">
A cloudy day in Singapore, the New Zealand trip 2019.
</p>
<div className="mt-4 flex items-center gap-3">
<span className="w-8 h-px bg-[#84c0a0]"></span>
<span className="text-[#84c0a0] font-mono text-xs tracking-widest uppercase">Expedition 019</span>
</div>
</div>
</div>
</div>
</div>
</section>
{/* 5. CONTACT CALL-TO-ACTION (Compact Banner) */}
<section className="relative py-16 px-6 overflow-hidden border-t border-white/5">
{/* Immersive Mountain Background */}
<img
src="/assets/images/snowy_mountain_peaks.jpg"
alt="Snowy Mountains"
className="absolute inset-0 w-full h-full object-cover z-0"
/>
{/* Dark overlay */}
<div className="absolute inset-0 bg-[#0d1117]/80 backdrop-blur-[2px] z-0"></div>
{/* Compact Glassmorphic CTA Banner */}
<div className="relative z-10 max-w-5xl mx-auto bg-white/5 backdrop-blur-xl border border-white/10 rounded-3xl p-8 md:px-12 md:py-10 shadow-2xl flex flex-col md:flex-row items-center justify-between gap-6">
<div className="text-center md:text-left">
<h2 className="text-white text-3xl md:text-4xl font-black uppercase tracking-tighter mb-2">
Let's Connect
</h2>
<p className="text-[#84c0a0] font-mono uppercase text-xs md:text-sm tracking-widest font-bold">
Transmission & Socials
</p>
</div>
<Link
href="/contact"
className="shrink-0 inline-block bg-[#84c0a0] text-[#0d1117] px-8 py-4 rounded-full font-bold uppercase tracking-wider hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(132,192,160,0.3)]"
>
Contact Me
</Link>
</div>
</section>
</div>
);
}
}
+277
View File
@@ -0,0 +1,277 @@
// app/projects/code/page.tsx
import MatrixRain from '@/app/components/MatrixRain';
import { FaCodeBranch, FaGithub, FaGitlab, FaGlobe, FaStar } from 'react-icons/fa';
interface GithubProfile {
html_url: string;
avatar_url: string;
name: string;
login: string;
bio: string;
followers: number;
public_repos: number;
}
interface GithubRepo {
id: number;
name: string;
description: string;
html_url: string;
language: string;
stargazers_count: number;
forks: number;
}
interface GitlabRepo {
id: number;
name: string;
description: string;
web_url: string;
star_count: number;
forks_count: number;
language?: string;
}
async function getProfile(): Promise<GithubProfile | null> {
try {
const res = await fetch('https://api.github.com/users/WilliamSoderberg', { next: { revalidate: 60 } });
if (!res.ok) return null;
return res.json();
} catch (e) {
return null;
}
}
async function getGithubRepos(): Promise<GithubRepo[]> {
try {
const res = await fetch('https://api.github.com/users/WilliamSoderberg/repos?sort=updated', { next: { revalidate: 60 } });
if (!res.ok) return [];
return res.json();
} catch (e) {
return [];
}
}
async function getGitlabRepos(): Promise<GitlabRepo[]> {
try {
const res = await fetch('https://gitlab.soderberg.tech/api/v4/groups/soderberg-tech/projects', {
next: { revalidate: 60 }
});
if (!res.ok) return [];
const projects: GitlabRepo[] = await res.json();
const projectsWithLanguages = await Promise.all(
projects.map(async (repo) => {
try {
const langRes = await fetch(`https://gitlab.soderberg.tech/api/v4/projects/${repo.id}/languages`, {
next: { revalidate: 60 }
});
if (langRes.ok) {
const languages = await langRes.json();
const topLanguage = Object.keys(languages).sort((a, b) => languages[b] - languages[a])[0];
return { ...repo, language: topLanguage };
}
} catch (e) {
console.error(`Failed to fetch language for GitLab repo: ${repo.id}`);
}
return repo;
})
);
return projectsWithLanguages;
} catch (e) {
console.error("GitLab fetch failed, returning empty array");
return [];
}
}
const langColors: Record<string, string> = {
JavaScript: 'bg-[#f1e05a]',
TypeScript: 'bg-[#2b7489]',
Python: 'bg-[#3572A5]',
PHP: 'bg-[#4F5D95]',
CSS: 'bg-[#563d7c]',
HTML: 'bg-[#e44b23]',
Vue: 'bg-[#41b883]'
};
export default async function CodePage() {
const [profile, githubRepos, gitlabRepos] = await Promise.all([
getProfile(),
getGithubRepos(),
getGitlabRepos()
]);
return (
<main className="flex flex-col flex-1 relative min-h-dvh">
<MatrixRain />
<div className="h-25 shrink-0"></div>
<div className="relative z-10 w-full max-w-350 mx-auto px-4 pb-20 flex flex-col gap-16">
{/* 2. Profile Card Section */}
{profile && (
<section className="flex justify-center mt-10">
<div className="relative flex flex-col justify-center items-center bg-linear-to-b from-[#333333]/90 to-[#2a2a2a]/90 backdrop-blur-lg rounded-3xl text-center p-8 shadow-[0_8px_32px_rgba(0,0,0,0.5)] w-full max-w-md border border-white/10">
<a href={profile.html_url} target="_blank" rel="noopener noreferrer">
<FaGithub className="absolute text-white text-[28px] top-5 right-5 hover:text-[#84c0a0] transition-colors" />
</a>
<div className="w-full">
<div className="relative h-37.5 w-37.5 mx-auto mb-5">
<img src={profile.avatar_url} alt="Avatar" className="h-full w-full rounded-full object-cover border-4 border-[#84c0a0] shadow-[0_0_15px_rgba(132,192,160,0.5)]" />
</div>
<h1 className="mb-1">
<a className="text-white text-[26px] font-bold tracking-wide no-underline hover:text-[#84c0a0] transition-colors" href={profile.html_url} target="_blank" rel="noopener noreferrer">
{profile.name}
</a>
</h1>
<div className="mb-5">
<a className="text-[#a0aab6] font-medium no-underline hover:text-white transition-colors" href={profile.html_url} target="_blank" rel="noopener noreferrer">
@{profile.login}
</a>
</div>
<p className="font-medium mx-auto text-[#84c0a0] mt-2.5 text-[15px] leading-relaxed">
{profile.bio}
</p>
<div className="mt-8 relative border-t border-white/10 pt-5 flex justify-around">
<div className="relative">
<div className="text-white text-[22px] font-bold">{profile.followers}</div>
<div className="text-[11px] font-bold text-[#a0aab6] tracking-widest uppercase mt-1">Followers</div>
</div>
<div className="relative">
<div className="text-white text-[22px] font-bold">{profile.public_repos}</div>
<div className="text-[11px] font-bold text-[#a0aab6] tracking-widest uppercase mt-1">Repositories</div>
</div>
</div>
</div>
</div>
</section>
)}
{/* 3. GitLab Repositories Section */}
<section className="bg-[#fc6d26]/10 backdrop-blur-lg rounded-3xl border border-[#fc6d26]/30 p-8 shadow-2xl">
<div className="flex justify-center items-center flex-col mb-10">
<a href="https://gitlab.soderberg.tech/WilliamSoderberg" target="_blank" rel="noopener noreferrer">
<FaGitlab className="text-[#fc6d26] text-[55px] drop-shadow-[0_0_15px_rgba(252,109,38,0.4)] hover:scale-110 transition-transform cursor-pointer" />
</a>
<h2 className="text-white mt-3.75 text-[32px] md:text-[38px] uppercase font-bold text-center tracking-wider">
GitLab Projects
</h2>
<p className="text-gray-300 text-center max-w-2xl mt-3">Self-hosted infrastructure and private development.</p>
</div>
{gitlabRepos.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 justify-center">
{gitlabRepos.map((repo) => (
<a key={repo.id} href={repo.web_url} target="_blank" rel="noopener noreferrer" className="group flex flex-col justify-between bg-linear-to-br from-[#2a2a2a]/90 to-[#222222]/90 border border-white/10 p-6 rounded-2xl hover:-translate-y-1 hover:border-[#fc6d26]/60 hover:shadow-[0_8px_24px_rgba(252,109,38,0.2)] transition-all duration-300 no-underline">
<div>
<h3 className="text-[#fc6d26] text-[19px] font-bold group-hover:text-white transition-colors truncate">
{repo.name}
</h3>
<p className="text-[14px] mb-4 mt-2.5 text-gray-400 text-left line-clamp-3 leading-relaxed">
{repo.description || 'No description provided.'}
</p>
</div>
<div className="flex items-center text-[13px] text-[#8b949e] mt-4 font-medium">
{/* NEW: GitLab Language Badge */}
{repo.language && (
<div className="flex items-center mr-5">
<span className={`w-2.5 h-2.5 rounded-full mr-2 ${langColors[repo.language] || 'bg-gray-500'}`}></span>
<span className="text-gray-300">{repo.language}</span>
</div>
)}
{repo.star_count > 0 && (
<span className="flex items-center mr-4">
<FaStar className="mr-1.5" /> {repo.star_count}
</span>
)}
{repo.forks_count > 0 && (
<span className="flex items-center">
<FaCodeBranch className="mr-1.5" /> {repo.forks_count}
</span>
)}
</div>
</a>
))}
</div>
) : (
<div className="text-center text-gray-400 italic py-8 bg-black/20 rounded-xl">No public GitLab repositories found or API is restricted.</div>
)}
</section>
{/* 4. GitHub Repositories Section */}
<section className="bg-[#84c0a0]/10 backdrop-blur-lg rounded-3xl border border-[#84c0a0]/30 p-8 shadow-2xl">
<div className="flex justify-center items-center flex-col mb-10">
<FaGithub className="text-white text-[55px] drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]" />
<h2 className="text-white mt-3.75 text-[32px] md:text-[38px] uppercase font-bold text-center tracking-wider">
Public Repositories
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 justify-center">
{githubRepos.map((repo) => (
<a key={repo.id} href={repo.html_url} target="_blank" rel="noopener noreferrer" className="group flex flex-col justify-between bg-linear-to-br from-[#2a2a2a]/90 to-[#222222]/90 border border-white/10 p-6 rounded-2xl hover:-translate-y-1 hover:border-[#84c0a0]/60 hover:shadow-[0_8px_24px_rgba(132,192,160,0.2)] transition-all duration-300 no-underline">
<div>
<h3 className="text-[#4e9fff] text-[19px] font-bold group-hover:text-white transition-colors truncate">
{repo.name}
</h3>
<p className="text-[14px] mb-4 mt-2.5 text-gray-400 text-left line-clamp-3 leading-relaxed">
{repo.description || 'No description provided.'}
</p>
</div>
<div className="flex items-center text-[13px] text-[#8b949e] mt-4 font-medium">
{repo.language && (
<div className="flex items-center mr-5">
<span className={`w-2.5 h-2.5 rounded-full mr-2 ${langColors[repo.language] || 'bg-gray-500'}`}></span>
<span className="text-gray-300">{repo.language}</span>
</div>
)}
{repo.stargazers_count > 0 && (
<span className="flex items-center mr-4">
<FaStar className="mr-1.5" /> {repo.stargazers_count}
</span>
)}
{repo.forks > 0 && (
<span className="flex items-center">
<FaCodeBranch className="mr-1.5" /> {repo.forks}
</span>
)}
</div>
</a>
))}
</div>
</section>
{/* 5. Hosted Websites Section */}
<section className="bg-white/5 backdrop-blur-lg rounded-3xl border border-white/10 p-10 shadow-2xl">
<div className="flex justify-center items-center flex-col mb-10">
<FaGlobe className="text-white text-[55px] drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]" />
<h2 className="text-white mt-3.75 text-[32px] md:text-[38px] uppercase font-bold text-center tracking-wider">
Hosted Websites
</h2>
</div>
{/* Changed from Flex to Grid for perfect centering! */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 max-w-5xl mx-auto justify-items-center items-center py-6">
<a className="no-underline text-white text-[22px] flex flex-col items-center hover:scale-105 transition-transform group" href="https://soderberg.tech">
<img src="/assets/logos/logo.svg" alt="Soderberg Tech" className="w-22.5 h-16.25 object-contain mb-5 drop-shadow-md group-hover:drop-shadow-[0_0_10px_rgba(255,255,255,0.5)] transition-all" />
<span className="font-bold tracking-wide text-center">Soderberg Tech</span>
</a>
<a className="no-underline text-white text-[22px] flex flex-col items-center hover:scale-105 transition-transform group" href="https://lirkab.se">
<img src="/assets/logos/lirkab.svg" alt="Lirkab" className="w-22.5 h-16.25 object-contain mb-5 drop-shadow-md group-hover:drop-shadow-[0_0_10px_rgba(255,255,255,0.5)] transition-all" />
<span className="font-bold tracking-wide text-center">Lirkab</span>
</a>
<a className="no-underline text-white text-[22px] flex flex-col items-center hover:scale-105 transition-transform group" href="https://lerbergetsvolleyboll.se">
<img src="/assets/logos/lvs.svg" alt="Lerbergets Volleyboll" className="w-22.5 h-16.25 object-contain mb-5 drop-shadow-md group-hover:drop-shadow-[0_0_10px_rgba(255,255,255,0.5)] transition-all" />
<span className="font-bold tracking-wide text-center">Lerbergets Volleyboll</span>
</a>
</div>
</section>
</div>
</main>
);
}
+156
View File
@@ -0,0 +1,156 @@
// app/projects/design/page.tsx
'use client';
import { Center, Environment, Loader, OrbitControls, useGLTF } from '@react-three/drei';
import { Canvas, useThree } from '@react-three/fiber';
import { Suspense, useEffect, useState, useTransition } from 'react';
import { TbCube3dSphere } from 'react-icons/tb';
import * as THREE from 'three';
const models = [
{ name: 'Wallet', src: 'Wallet' },
{ name: 'Car', src: 'Car' },
{ name: 'Magnet holder', src: 'Magnet holder' },
{ name: 'PC', src: 'PC' },
];
function Model({ url, onReady }: { url: string; onReady: () => void }) {
const { scene } = useGLTF(url);
useEffect(() => {
if (scene) {
const timer = setTimeout(onReady, 100);
return () => clearTimeout(timer);
}
}, [scene, onReady]);
return <primitive object={scene} />;
}
function DynamicCamera({ radius }: { radius: number }) {
const { camera } = useThree();
useEffect(() => {
camera.position.set(0, radius * 0.5, radius * 2.5);
camera.lookAt(0, 0, 0);
camera.updateProjectionMatrix();
}, [radius, camera]);
return null;
}
export default function DesignPage() {
const [activeModel, setActiveModel] = useState(models[0].src);
const [modelRadius, setModelRadius] = useState(1);
const [isPending, startTransition] = useTransition();
const [isSceneReady, setIsSceneReady] = useState(false);
const [isCentering, setIsCentering] = useState(true);
const handleModelChange = (newModel: string) => {
if (newModel === activeModel) return;
setIsCentering(true);
setIsSceneReady(false);
startTransition(() => {
setActiveModel(newModel);
});
};
const showOverlay = isPending || !isSceneReady;
return (
<main className="flex flex-col flex-1 relative bg-[#0d1117] overflow-hidden">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-200 h-200 bg-[#84c0a0]/10 blur-[150px] rounded-full pointer-events-none z-0"></div>
<div className="h-25 shrink-0"></div>
<div className="relative z-10 w-full max-w-350 mx-auto px-4 pb-20 flex flex-col gap-10">
<div className="pt-5 flex justify-center items-center flex-col text-center">
<TbCube3dSphere className="text-white text-[80px] drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]" />
<h2 className="text-white text-[32px] md:text-[45px] uppercase mt-3.75 font-bold tracking-wider">CAD Library</h2>
</div>
<section className="w-full flex justify-center">
<div className="w-full relative bg-white/5 backdrop-blur-xl rounded-4xl border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)] p-4 md:p-8 flex flex-col">
<div className="w-full h-[50vh] md:h-[65vh] rounded-[20px] overflow-hidden relative bg-linear-to-br from-[#1a1a1a] to-[#0f0f0f] shadow-inner border border-white/5 cursor-grab active:cursor-grabbing">
{showOverlay && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-[#0d1117]/80 backdrop-blur-md transition-all duration-500">
<div className="w-12 h-12 border-4 border-[#84c0a0]/20 border-t-[#84c0a0] rounded-full animate-spin mb-4"></div>
<p className="text-[#84c0a0] font-mono text-xs uppercase tracking-[0.2em] font-bold">
{isPending ? "Fetching Data..." : "Compiling Materials..."}
</p>
</div>
)}
<Canvas shadows={{ type: THREE.PCFShadowMap }} dpr={[1, 2]} camera={{ near: 0.001, far: 1000, fov: 50 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 10]} intensity={1} castShadow />
<Environment preset="city" />
<DynamicCamera radius={modelRadius} />
<Suspense fallback={null}>
<Center
key={activeModel}
onCentered={({ boundingSphere }) => {
setModelRadius(boundingSphere.radius);
requestAnimationFrame(() => setIsCentering(false));
}}
>
<group visible={!isCentering && isSceneReady}>
<Model
url={`/models/${activeModel}.glb`}
onReady={() => setIsSceneReady(true)}
/>
</group>
</Center>
</Suspense>
<OrbitControls
makeDefault
autoRotate
autoRotateSpeed={0.5}
enablePan={true}
enableZoom={true}
minDistance={modelRadius * 0.5}
maxDistance={modelRadius * 6}
/>
</Canvas>
<Loader
containerStyles={{ background: 'transparent' }}
innerStyles={{ display: 'none' }}
/>
<a href={`/models/${activeModel}.usdz`} rel="ar" className="absolute top-4 right-4 bg-black/50 backdrop-blur-md text-white text-sm px-4 py-2 rounded-full border border-white/10 hover:bg-white/20 transition-colors md:hidden z-10 flex items-center gap-2">
<img src={`/models/${activeModel}.png`} alt="AR Preview" className="w-5 h-5 rounded-full object-cover" />
View in AR
</a>
</div>
<div className="w-full mt-8 flex justify-center">
<div className="flex flex-wrap justify-center gap-4 md:gap-8 w-full px-2">
{models.map((model) => (
<button
key={model.name}
className={`shrink-0 w-20 h-20 md:w-25 md:h-25 bg-contain bg-no-repeat bg-center rounded-2xl transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#84c0a0] relative group ${activeModel === model.src
? 'bg-[#2a2a2a] border-2 border-[#84c0a0] scale-110 shadow-[0_0_20px_rgba(132,192,160,0.3)]'
: 'bg-[#1a1a1a] border border-white/10 hover:bg-[#252525] hover:-translate-y-1'
}`}
style={{ backgroundImage: `url('/models/${model.src}.png')` }}
onClick={() => handleModelChange(model.src)}
onMouseEnter={() => useGLTF.preload(`/models/${model.src}.glb`)}
aria-label={`View ${model.name} model`}
>
<span className="absolute -bottom-8 left-1/2 -translate-x-1/2 text-xs font-semibold text-white opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap bg-black/80 px-2 py-1 rounded">
{model.name}
</span>
</button>
))}
</div>
</div>
</div>
</section>
</div>
</main>
);
}
+61
View File
@@ -0,0 +1,61 @@
// app/projects/page.tsx
import Link from 'next/link';
import BlurredCode from '@/app/components/BlurredCode';
import { TbCube3dSphere } from 'react-icons/tb';
import { FaCode } from 'react-icons/fa';
export default function ProjectsPage() {
return (
<main className="flex flex-col flex-1 relative bg-[#0d1117] overflow-x-hidden">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-150 md:w-250 h-150 md:h-250 bg-[#84c0a0]/10 blur-[150px] rounded-full pointer-events-none z-0"></div>
<div className="h-25 shrink-0"></div>
<div className="relative z-10 flex-1 flex flex-col justify-center items-center py-10 md:py-20 px-6">
<div className="mb-12 text-center">
<h1 className="text-white text-[32px] md:text-[55px] uppercase font-extrabold tracking-wider drop-shadow-lg">
My Portfolios
</h1>
<p className="text-[#a0aab6] mt-2 text-base md:text-xl max-w-md mx-auto">
Choose a discipline to explore my work.
</p>
</div>
<section className="flex flex-col md:flex-row gap-8 md:gap-12 w-full max-w-275 items-stretch">
<Link
href="/projects/code"
className="relative flex-1 group min-h-75 md:h-125 rounded-4xl overflow-hidden border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)] transition-all duration-500 hover:-translate-y-2 hover:border-[#84c0a0]/60 no-underline"
>
<BlurredCode />
<div className="absolute inset-0 z-10 bg-[#0d1117]/60 backdrop-blur-[2px] group-hover:bg-[#0d1117]/20 group-hover:backdrop-blur-[1px] transition-all duration-500 flex flex-col justify-center items-center p-8">
<FaCode className="text-white text-[50px] md:text-[80px] mb-4 md:mb-6 opacity-80 group-hover:opacity-100 group-hover:scale-110 group-hover:text-[#84c0a0] transition-all duration-500" />
<h2 className="text-white text-[32px] md:text-[50px] font-bold text-center tracking-wide">
Coding
</h2>
</div>
</Link>
<Link
href="/projects/design"
className="relative flex-1 group min-h-75 md:h-125 rounded-4xl overflow-hidden border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)] transition-all duration-500 hover:-translate-y-2 hover:border-[#84c0a0]/60 no-underline"
>
<div
className="absolute inset-0 z-0 bg-center bg-cover bg-no-repeat opacity-40 group-hover:opacity-100 group-hover:scale-105 transition-all duration-700"
style={{ backgroundImage: `url('/assets/images/CAD.png')` }}
></div>
<div className="absolute inset-0 z-10 bg-[#0d1117]/60 backdrop-blur-[2px] group-hover:bg-[#0d1117]/20 group-hover:backdrop-blur-[1px] transition-all duration-500 flex flex-col justify-center items-center p-8">
<TbCube3dSphere className="text-white text-[50px] md:text-[80px] mb-4 md:mb-6 opacity-80 group-hover:opacity-100 group-hover:scale-110 group-hover:text-[#84c0a0] transition-all duration-500" />
<h2 className="text-white text-[32px] md:text-[50px] font-bold text-center tracking-wide">
CAD & Design
</h2>
</div>
</Link>
</section>
</div>
</main>
);
}
+5 -1
View File
@@ -1,7 +1,11 @@
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
qualities: [75, 80, 85, 90, 95, 100], // Add 90 here!
},
};
export default nextConfig;
+992 -14
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -9,15 +9,25 @@
"lint": "eslint"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"image-size": "^2.0.2",
"next": "16.1.6",
"photoswipe": "^5.4.4",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"react-icons": "^5.6.0",
"react-photoswipe-gallery": "^4.0.0",
"react-syntax-highlighter": "^16.1.1",
"three": "^0.183.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/image-size": "^0.7.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",