Main pages
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
// app/css.d.ts
|
||||||
|
|
||||||
|
declare module '*.css';
|
||||||
@@ -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
@@ -1,26 +1,25 @@
|
|||||||
|
/* app/globals.css */
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
@keyframes zoomIn {
|
||||||
--background: #ffffff;
|
0% {
|
||||||
--foreground: #171717;
|
opacity: 0;
|
||||||
|
transform: scale3d(.3, .3, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@keyframes fadeIn {
|
||||||
--color-background: var(--background);
|
0% {
|
||||||
--color-foreground: var(--foreground);
|
opacity: 0;
|
||||||
--font-sans: var(--font-geist-sans);
|
}
|
||||||
--font-mono: var(--font-geist-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
100% {
|
||||||
:root {
|
opacity: 1;
|
||||||
--background: #0a0a0a;
|
}
|
||||||
--foreground: #ededed;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
Vendored
+22
@@ -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
@@ -1,34 +1,35 @@
|
|||||||
import type { Metadata } from "next";
|
// app/layout.tsx
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
import Navbar from './components/Navbar';
|
||||||
variable: "--font-geist-sans",
|
import './globals.css';
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
export const metadata = {
|
||||||
variable: "--font-geist-mono",
|
title: 'Soderberg Tech',
|
||||||
subsets: ["latin"],
|
description: 'William Söderberg, Technical high school engineer from Höganäs, Sweden',
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Create Next App",
|
|
||||||
description: "Generated by create next app",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className="flex flex-col min-h-screen m-0 p-0 font-sans bg-[#0d1117]">
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
<Navbar />
|
||||||
>
|
|
||||||
{children}
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+217
-57
@@ -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() {
|
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 (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="relative w-full bg-[#0d1117] text-white overflow-hidden font-sans">
|
||||||
<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
|
{/* 1. HERO SECTION */}
|
||||||
className="dark:invert"
|
<section className="relative h-screen min-h-175 flex flex-col justify-center items-center overflow-hidden">
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
{/* Ambient Overlay for contrast */}
|
||||||
width={100}
|
<div className="absolute inset-0 bg-linear-to-b from-transparent via-[#0d1117]/50 to-[#0d1117] z-1"></div>
|
||||||
height={20}
|
|
||||||
priority
|
<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]">
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
<img src="/assets/logos/logo.svg" alt="Logo" className="h-[60%]" />
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
</div>
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
|
<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>
|
</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{" "}
|
<div className="mt-10 flex gap-4">
|
||||||
<a
|
<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">
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
View Portfolios
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
</Link>
|
||||||
>
|
</div>
|
||||||
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>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
</section>
|
||||||
<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]"
|
{/* 2. ABOUT ME SECTION */}
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<section className="relative py-24 px-6">
|
||||||
target="_blank"
|
{/* Background Glow */}
|
||||||
rel="noopener noreferrer"
|
<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>
|
||||||
>
|
|
||||||
<Image
|
<div className="max-w-6xl mx-auto">
|
||||||
className="dark:invert"
|
{/* Subtitle Header */}
|
||||||
src="/vercel.svg"
|
<div className="text-center mb-12">
|
||||||
alt="Vercel logomark"
|
<h2 className="text-white text-4xl md:text-5xl font-black uppercase tracking-tighter">About Me</h2>
|
||||||
width={16}
|
<p className="text-[#84c0a0] mt-4 font-mono uppercase text-sm tracking-widest font-bold">Identity & Background</p>
|
||||||
height={16}
|
</div>
|
||||||
/>
|
|
||||||
Deploy Now
|
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-[40px] overflow-hidden flex flex-col lg:flex-row shadow-2xl">
|
||||||
</a>
|
|
||||||
<a
|
{/* Left Column: Profile with "Scanning" Effect */}
|
||||||
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]"
|
<div className="lg:w-2/5 relative group border-b lg:border-b-0 lg:border-r border-white/10">
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<img
|
||||||
target="_blank"
|
src="/assets/images/profile_picture.jpg"
|
||||||
rel="noopener noreferrer"
|
alt="William Söderberg"
|
||||||
>
|
className="w-full h-full object-cover"
|
||||||
Documentation
|
/>
|
||||||
</a>
|
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -1,7 +1,11 @@
|
|||||||
|
// next.config.ts
|
||||||
|
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
qualities: [75, 80, 85, 90, 95, 100], // Add 90 here!
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+992
-14
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -9,15 +9,25 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-three/drei": "^10.7.7",
|
||||||
|
"@react-three/fiber": "^9.5.0",
|
||||||
|
"image-size": "^2.0.2",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"photoswipe": "^5.4.4",
|
||||||
"react": "19.2.3",
|
"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": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/image-size": "^0.7.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
Reference in New Issue
Block a user