mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
Upgrade Next.js to 15, fix first-load theme bug, add SVG assets
- Upgrade next 14.1.0 → 15.5.9, next-themes 0.2.1 → 0.4.6 - Fix theme not initializing on first load after server restart - Add SVG logos and favicon, update site-logo to use SVGs - Create shared Header component with skip-to-main link - Add portfolio links to bento grid cards - Improve accessibility: aria-describedby, prefers-reduced-motion - Fix useFormState → useActionState for React 19 compatibility - Fix founder-signature icon (use square favicon instead of wide logo)
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { FounderSignature } from "@/components/about/founder-signature";
|
||||
import { SiteLogo } from "@/components/layout/site-logo";
|
||||
import Link from "next/link";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
|
||||
import { FaXTwitter, FaTelegram, FaReddit } from "react-icons/fa6";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -19,19 +17,9 @@ export const metadata: Metadata = {
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<main className="relative flex flex-col items-center overflow-hidden">
|
||||
|
||||
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<SiteLogo />
|
||||
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
||||
<Link href="/about" className="text-primary transition-colors">About</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
<Header />
|
||||
|
||||
<div className="z-10 w-full max-w-6xl mx-auto py-20 px-4">
|
||||
<div id="main-content" className="z-10 w-full max-w-6xl mx-auto py-20 px-4">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-10 text-center">About Us</h1>
|
||||
|
||||
<div className="section-container">
|
||||
@@ -78,8 +66,21 @@ export default function AboutPage() {
|
||||
<div className="flex flex-col justify-center items-center bg-muted/30 rounded-xl p-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold mb-2">Connect</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">Find us on your favorite platforms</p>
|
||||
{/* Social links are in the footer, but we can highlight them here or leave for footer */}
|
||||
<p className="text-sm text-muted-foreground mb-4">Find us on your favorite platforms</p>
|
||||
<div className="flex gap-6 justify-center">
|
||||
<a href="https://x.com/such_software" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-primary transition-colors">
|
||||
<FaXTwitter className="h-6 w-6" />
|
||||
<span className="sr-only">X (Twitter)</span>
|
||||
</a>
|
||||
<a href="https://t.me/j_winter_m" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-primary transition-colors">
|
||||
<FaTelegram className="h-6 w-6" />
|
||||
<span className="sr-only">Telegram</span>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/SuchSoftware" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-primary transition-colors">
|
||||
<FaReddit className="h-6 w-6" />
|
||||
<span className="sr-only">Reddit</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
overflow: hidden;
|
||||
transition: all 300ms;
|
||||
}
|
||||
|
||||
|
||||
.section-container {
|
||||
@apply mask-backdrop rounded-[2.5rem] p-12 md:p-20 text-card-foreground;
|
||||
}
|
||||
@@ -95,8 +95,20 @@
|
||||
.glass-card {
|
||||
@apply mask-backdrop rounded-[2rem] p-10 text-card-foreground;
|
||||
}
|
||||
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect users who prefer reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ export const metadata: Metadata = {
|
||||
description: "Next-generation web applications engineered for scale, compliance, and performance.",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/images/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
|
||||
],
|
||||
shortcut: "/images/favicon_192.png",
|
||||
shortcut: "/images/favicon.svg",
|
||||
apple: [
|
||||
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
|
||||
],
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { PortfolioGrid } from "@/components/portfolio/bento-grid";
|
||||
import { ContactForm } from "@/components/contact/contact-form";
|
||||
import { FounderSignature } from "@/components/about/founder-signature";
|
||||
import { ServicesPreview } from "@/components/landing/services-preview";
|
||||
import { SiteLogo } from "@/components/layout/site-logo";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { Card } from "@repo/ui/components/card";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="relative flex flex-col items-center justify-between overflow-hidden">
|
||||
|
||||
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<SiteLogo />
|
||||
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
||||
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
<div className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
|
||||
<Header />
|
||||
|
||||
<div id="main-content" className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
|
||||
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500 pb-2">
|
||||
Digital Craftsmanship
|
||||
</h1>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DataGrid } from "@repo/ui/procedural/data-grid";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { SiteLogo } from "@/components/layout/site-logo";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import Link from "next/link";
|
||||
import { Card } from "@repo/ui/components/card";
|
||||
import type { Metadata } from "next";
|
||||
@@ -19,22 +18,10 @@ export const metadata: Metadata = {
|
||||
export default function ServicesPage() {
|
||||
return (
|
||||
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground overflow-hidden">
|
||||
|
||||
{/* Option B: Data Grid Background for Technical Feel */}
|
||||
<DataGrid />
|
||||
<Header />
|
||||
|
||||
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 border-b border-border/50 rounded-b-3xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<SiteLogo />
|
||||
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||
<Link href="/services" className="text-primary transition-colors">Services</Link>
|
||||
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
<div className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
|
||||
<div id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
|
||||
{/* Context Header */}
|
||||
<div className="mb-20">
|
||||
<div className="text-left mb-8 section-container border-l-4 border-l-emerald-500">
|
||||
@@ -70,8 +57,8 @@ export default function ServicesPage() {
|
||||
<Card className="glass-card hover:border-indigo-500/30">
|
||||
<h3 className="text-xl font-bold mb-3">Zero Capital Upfront</h3>
|
||||
<p className="text-base text-muted-foreground leading-relaxed">
|
||||
Our "Toast" style partnership means we win when you win. Low friction onboarding
|
||||
with performance-based incentives.
|
||||
Our revenue-share partnership means no setup fees or monthly subscriptions—we
|
||||
only earn when you're making sales.
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="glass-card hover:border-indigo-500/30">
|
||||
|
||||
@@ -7,12 +7,13 @@ export const FounderSignature = () => {
|
||||
{/* Left Column: Bio */}
|
||||
<div className="space-y-8 text-left section-container">
|
||||
<div className="flex items-center gap-6 relative z-10">
|
||||
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-primary/10 bg-white dark:bg-zinc-900 shadow-xl ring-4 ring-primary/5">
|
||||
<Image
|
||||
src="/images/logo_w_circle_trans.jpg"
|
||||
alt="Such Software Icon"
|
||||
fill
|
||||
className="object-cover scale-[1.02]"
|
||||
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-primary/10 bg-white dark:bg-zinc-900 shadow-xl ring-4 ring-primary/5 flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/favicon.svg"
|
||||
alt="Such Software Icon"
|
||||
width={72}
|
||||
height={72}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -23,13 +24,13 @@ export const FounderSignature = () => {
|
||||
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<p className="text-muted-foreground leading-relaxed italic border-l-4 border-primary/20 pl-6 text-lg">
|
||||
"During my three years at <span className="text-foreground font-semibold">Cecil College</span>, I didn't just teach web engineering—I re-engineered how it was taught.
|
||||
"During my three years at <span className="text-foreground font-semibold">Cecil College</span>, I didn't just teach engineering—I re-engineered how it was taught.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-6">
|
||||
Transitioning the curriculum toward from PDFs and Powerpoints to web-based content, I focused on transforming complex technical lessons into highly accessible HTML using <span className="text-foreground font-semibold underline decoration-primary/50">Quarto markdown</span> as our source. This allowed us to consistently generate semantic content including accessible equations and guaranteed descriptive alt text for every visual asset.
|
||||
Transitioning the curriculum from PDFs and Powerpoints to web-based content, I focused on transforming complex technical lessons into highly accessible HTML using <span className="text-foreground font-semibold underline decoration-primary/50">Quarto markdown</span>. This allowed us to consistently generate semantic content including accessible equations and guaranteed descriptive alt text for every visual asset.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-4 italic border-l-4 border-primary/20 pl-6 text-lg">
|
||||
This wasn't just academic theory; it was a rigorous discipline of semantic architecture, ARIA compliance, and cognitive load management. Today, I bring that same technical rigor to Such Software’s commercial projects. We don't retroactively fix accessibility; we architect for it from line one."
|
||||
This wasn't just theory; it was a methodical implementation of semantic architecture, ARIA compliance, and cognitive load management. Today, I bring that same technical rigor to Such Software’s commercial projects. We don't retroactively fix accessibility; we architect for it from line one."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,18 +62,18 @@ export const FounderSignature = () => {
|
||||
<span className="text-emerald-500 text-[10px]">03</span>
|
||||
Core Competencies
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 gap-2 font-mono text-[11px] uppercase tracking-wider text-muted-foreground/80">
|
||||
<ul className="grid grid-cols-1 gap-2 font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-emerald-500 text-sm">✓</span> WCAG 2.2 AA Compliance
|
||||
<span className="text-emerald-500">✓</span> WCAG 2.2 AA Compliance
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-emerald-500 text-sm">✓</span> Section 508 & ADA
|
||||
<span className="text-emerald-500">✓</span> Section 508 & ADA
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-emerald-500 text-sm">✓</span> Semantic HTML5 Architecture
|
||||
<span className="text-emerald-500">✓</span> Semantic HTML5 Architecture
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-emerald-500 text-sm">✓</span> Playwright A11y Automation
|
||||
<span className="text-emerald-500">✓</span> Playwright A11y Automation
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
'use client';
|
||||
import { useFormState, useFormStatus } from 'react-dom';
|
||||
import { useActionState } from 'react';
|
||||
import { useFormStatus } from 'react-dom';
|
||||
import { submitContactForm } from '@/actions/submit-contact';
|
||||
import { TurnstileWidget } from './turnstile-widget';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from '@repo/ui/components/button';
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
size="lg"
|
||||
className="w-full md:w-64 rounded-xl text-lg font-semibold"
|
||||
>
|
||||
@@ -20,12 +21,22 @@ function SubmitButton() {
|
||||
}
|
||||
|
||||
export function ContactForm() {
|
||||
const [state, formAction] = useFormState(submitContactForm, { success: false, message: '' });
|
||||
const [state, formAction] = useActionState(submitContactForm, { success: false, message: '' });
|
||||
const [token, setToken] = useState<string>('');
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
// Reset form on successful submission
|
||||
useEffect(() => {
|
||||
if (state.success && formRef.current) {
|
||||
formRef.current.reset();
|
||||
setToken('');
|
||||
}
|
||||
}, [state.success]);
|
||||
|
||||
return (
|
||||
<form
|
||||
action={formAction}
|
||||
<form
|
||||
ref={formRef}
|
||||
action={formAction}
|
||||
className="space-y-8 w-full max-w-6xl mx-auto section-container"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||
@@ -41,40 +52,46 @@ export function ContactForm() {
|
||||
<div className="grid md:grid-cols-2 gap-8 relative z-10">
|
||||
<div className="space-y-3">
|
||||
<label htmlFor="name" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Full Name</label>
|
||||
<input
|
||||
name="name"
|
||||
id="name"
|
||||
<input
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Jamie Doe"
|
||||
required
|
||||
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
|
||||
required
|
||||
aria-describedby={state?.errors?.name ? "name-error" : undefined}
|
||||
aria-invalid={state?.errors?.name ? "true" : undefined}
|
||||
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
|
||||
/>
|
||||
{state?.errors?.name && <p className="text-red-500 text-sm px-1">{state.errors.name}</p>}
|
||||
{state?.errors?.name && <p id="name-error" className="text-red-500 text-sm px-1" role="alert">{state.errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label htmlFor="email" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Work Email</label>
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
type="email"
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="jamie@example.com"
|
||||
required
|
||||
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
|
||||
required
|
||||
aria-describedby={state?.errors?.email ? "email-error" : undefined}
|
||||
aria-invalid={state?.errors?.email ? "true" : undefined}
|
||||
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
|
||||
/>
|
||||
{state?.errors?.email && <p className="text-red-500 text-sm px-1">{state.errors.email}</p>}
|
||||
{state?.errors?.email && <p id="email-error" className="text-red-500 text-sm px-1" role="alert">{state.errors.email}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 relative z-10">
|
||||
<label htmlFor="message" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Project Details</label>
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
placeholder="What are you building? Mention any specific constraints, timelines, or technologies..."
|
||||
required
|
||||
className="w-full p-6 border-2 border-border/50 rounded-2xl bg-background/50 h-56 resize-none focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg leading-relaxed"
|
||||
required
|
||||
aria-describedby={state?.errors?.message ? "message-error" : undefined}
|
||||
aria-invalid={state?.errors?.message ? "true" : undefined}
|
||||
className="w-full p-6 border-2 border-border/50 rounded-2xl bg-background/50 h-56 resize-none focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg leading-relaxed"
|
||||
/>
|
||||
{state?.errors?.message && <p className="text-red-500 text-sm px-1">{state.errors.message}</p>}
|
||||
{state?.errors?.message && <p id="message-error" className="text-red-500 text-sm px-1" role="alert">{state.errors.message}</p>}
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="token" value={token} />
|
||||
@@ -92,13 +109,17 @@ export function ContactForm() {
|
||||
</div>
|
||||
|
||||
{state?.message && (
|
||||
<div className={`mt-8 p-6 rounded-2xl text-base border-l-4 relative z-10 ${
|
||||
state.success
|
||||
? "bg-emerald-500/10 border-emerald-500 text-emerald-700 dark:text-emerald-400"
|
||||
: "bg-red-500/10 border-red-500 text-red-700 dark:text-red-400"
|
||||
}`}>
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={`mt-8 p-6 rounded-2xl text-base border-l-4 relative z-10 ${
|
||||
state.success
|
||||
? "bg-emerald-500/10 border-emerald-500 text-emerald-700 dark:text-emerald-400"
|
||||
: "bg-red-500/10 border-red-500 text-red-700 dark:text-red-400"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">{state.success ? "✓" : "⚠"}</span>
|
||||
<span className="text-xl" aria-hidden="true">{state.success ? "✓" : "⚠"}</span>
|
||||
<p className="font-medium">{state.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,19 +5,31 @@ import { NebulaField } from "@repo/ui/procedural/nebula-field";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function DynamicBackground() {
|
||||
const { theme } = useTheme();
|
||||
const { resolvedTheme, theme, systemTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
// Debug on first load
|
||||
useEffect(() => {
|
||||
console.log('[DynamicBackground] mounted:', mounted, 'theme:', theme, 'resolvedTheme:', resolvedTheme, 'systemTheme:', systemTheme);
|
||||
}, [mounted, theme, resolvedTheme, systemTheme]);
|
||||
|
||||
// Don't render until mounted (standard hydration fix)
|
||||
if (!mounted) {
|
||||
return <div className="fixed inset-0 z-0 h-screen w-screen" />;
|
||||
}
|
||||
|
||||
// After mount, determine the theme - fall back to system preference or 'dark'
|
||||
const effectiveTheme = resolvedTheme || systemTheme || 'dark';
|
||||
const themeMode = effectiveTheme === 'dark' ? 'dark' : 'light';
|
||||
|
||||
return (
|
||||
<NebulaField
|
||||
className="fixed inset-0 z-0 h-screen w-screen"
|
||||
themeMode={theme === 'dark' ? 'dark' : 'light'}
|
||||
<NebulaField
|
||||
className="fixed inset-0 z-0 h-screen w-screen"
|
||||
themeMode={themeMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { SiteLogo } from "./site-logo";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
export function Header() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
|
||||
<div className="flex items-center gap-4">
|
||||
<SiteLogo />
|
||||
<nav className="hidden md:flex gap-6 text-sm font-medium" aria-label="Main navigation">
|
||||
<Link
|
||||
href="/services"
|
||||
className={pathname === "/services" ? "text-primary" : "hover:text-primary transition-colors"}
|
||||
>
|
||||
Services
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className={pathname === "/about" ? "text-primary" : "hover:text-primary transition-colors"}
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import Link from "next/link";
|
||||
export function SiteLogo() {
|
||||
return (
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div className="relative w-52 h-14 lg:w-64 lg:h-16">
|
||||
<div className="relative w-40 h-11 lg:w-48 lg:h-12">
|
||||
{/* Dark mode logo (white text) */}
|
||||
<Image
|
||||
src="/images/logo_dark_w_name_trans.png"
|
||||
src="/images/logo_dark_w_name.svg"
|
||||
alt="Such Software"
|
||||
fill
|
||||
className="object-contain hidden dark:block"
|
||||
@@ -17,7 +17,7 @@ export function SiteLogo() {
|
||||
/>
|
||||
{/* Light mode logo (black text) */}
|
||||
<Image
|
||||
src="/images/logo_light_w_name_trans.png"
|
||||
src="/images/logo_light_w_name.svg"
|
||||
alt="Such Software"
|
||||
fill
|
||||
className="object-contain block dark:hidden"
|
||||
|
||||
@@ -1,81 +1,90 @@
|
||||
import { Card } from "@repo/ui/components/card";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export const PortfolioGrid = () => {
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-6 w-full max-w-7xl mx-auto p-4 z-10 relative">
|
||||
{/* Hero Project: r/CryptoCurrency */}
|
||||
<Card className="md:col-span-2 md:row-span-2 glass-card group min-h-[300px] rounded-[2.5rem] !bg-slate-950/90">
|
||||
<div className="absolute inset-0 z-20 bg-gradient-to-t from-black/95 via-black/20 to-transparent p-12 flex flex-col justify-end items-center text-center pointer-events-none pb-20">
|
||||
<h3 className="text-4xl font-bold text-white shadow-black drop-shadow-xl tracking-tight">r/CryptoCurrency</h3>
|
||||
<p className="text-gray-200 shadow-black drop-shadow-md max-w-md mt-3 font-medium text-lg leading-relaxed">
|
||||
Governance dashboard for 10M+ members. Real-time DAO metrics.
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30">
|
||||
<Image
|
||||
src="/images/r-cryptocurrency.png"
|
||||
alt="r/CryptoCurrency"
|
||||
fill
|
||||
className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Link href="https://rcryptocurrency.com" target="_blank" rel="noopener noreferrer" className="md:col-span-2 md:row-span-2">
|
||||
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-slate-950/90 cursor-pointer">
|
||||
<div className="absolute inset-0 z-20 bg-gradient-to-t from-black/95 via-black/20 to-transparent p-12 flex flex-col justify-end items-center text-center pointer-events-none pb-20">
|
||||
<h3 className="text-4xl font-bold text-white shadow-black drop-shadow-xl tracking-tight">r/CryptoCurrency</h3>
|
||||
<p className="text-gray-200 shadow-black drop-shadow-md max-w-md mt-3 font-medium text-lg leading-relaxed">
|
||||
Governance dashboard for 10M+ members. Real-time DAO metrics.
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30">
|
||||
<Image
|
||||
src="/images/r-cryptocurrency.png"
|
||||
alt="Screenshot of r/CryptoCurrency subreddit homepage showing over 10 million subscribers, community links, and a cartoon moon mascot"
|
||||
fill
|
||||
className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* Secondary: Occupy Wallets */}
|
||||
<Card className="md:col-span-2 glass-card group min-h-[300px] rounded-[2.5rem]">
|
||||
<div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none">
|
||||
<h3 className="text-2xl font-bold text-zinc-900 dark:text-white drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2">Occupy Wallets</h3>
|
||||
<p className="text-zinc-900 dark:text-zinc-200 font-bold drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2 mt-4">High-fidelity gallery for fiscal graffiti.</p>
|
||||
</div>
|
||||
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50">
|
||||
<Image
|
||||
src="/images/occupy-wallets.png"
|
||||
alt="Occupy Wallets"
|
||||
fill
|
||||
className="object-contain p-4 opacity-60 transition-transform duration-1000 group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Link href="https://occupywallets.art" target="_blank" rel="noopener noreferrer" className="md:col-span-2">
|
||||
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] cursor-pointer">
|
||||
<div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none">
|
||||
<h3 className="text-2xl font-bold text-zinc-900 dark:text-white drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2">Occupy Wallets</h3>
|
||||
<p className="text-zinc-900 dark:text-zinc-200 font-bold drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2 mt-4">High-fidelity gallery for fiscal graffiti.</p>
|
||||
</div>
|
||||
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50">
|
||||
<Image
|
||||
src="/images/occupy-wallets.png"
|
||||
alt="Screenshot of Occupy Wallets gallery displaying illustrated artwork with political and economic commentary"
|
||||
fill
|
||||
className="object-contain p-4 opacity-60 transition-transform duration-1000 group-hover:scale-105"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
||||
<Card className="md:col-span-1 glass-card group min-h-[300px] rounded-[2.5rem] !bg-orange-50/95 dark:!bg-orange-950/95 !border-orange-500/20">
|
||||
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
|
||||
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
|
||||
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
|
||||
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li>
|
||||
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-orange-500/10 shadow-inner">
|
||||
<Image
|
||||
src="/images/privacy-labs.png"
|
||||
alt="Privacy Labs"
|
||||
fill
|
||||
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
|
||||
sizes="(max-width: 768px) 100vw, 25vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Link href="https://play.wowne.ro" target="_blank" rel="noopener noreferrer" className="md:col-span-1">
|
||||
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-orange-50/95 dark:!bg-orange-950/95 !border-orange-500/20 cursor-pointer">
|
||||
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
|
||||
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
|
||||
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
|
||||
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li>
|
||||
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-orange-500/10 shadow-inner">
|
||||
<Image
|
||||
src="/images/privacy-labs.png"
|
||||
alt="Screenshots of Monerogue roguelike game and Wowne.ro WebAssembly wallet interface"
|
||||
fill
|
||||
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
|
||||
sizes="(max-width: 768px) 100vw, 25vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* Quaternary: MoonPlace */}
|
||||
<Card className="md:col-span-1 glass-card group min-h-[300px] rounded-[2.5rem] !bg-indigo-50/95 dark:!bg-indigo-950/95 !border-indigo-500/20">
|
||||
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
|
||||
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
|
||||
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
|
||||
</div>
|
||||
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-indigo-500/10 shadow-inner">
|
||||
<Image
|
||||
src="/images/moonplace.png"
|
||||
alt="MoonPlace"
|
||||
fill
|
||||
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
|
||||
sizes="(max-width: 768px) 100vw, 25vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Link href="https://moonplace.io" target="_blank" rel="noopener noreferrer" className="md:col-span-1">
|
||||
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-indigo-50/95 dark:!bg-indigo-950/95 !border-indigo-500/20 cursor-pointer">
|
||||
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
|
||||
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
|
||||
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
|
||||
</div>
|
||||
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-indigo-500/10 shadow-inner">
|
||||
<Image
|
||||
src="/images/moonplace.png"
|
||||
alt="Screenshot of MoonPlace collaborative pixel art canvas showing colorful community-created artwork"
|
||||
fill
|
||||
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
|
||||
sizes="(max-width: 768px) 100vw, 25vw"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
import { type ThemeProviderProps } from "next-themes"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
|
||||
@@ -6,18 +6,24 @@ import { Button } from "@repo/ui/components/button";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const { setTheme, resolvedTheme, theme, systemTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Debug
|
||||
useEffect(() => {
|
||||
console.log('[ThemeToggle] mounted:', mounted, 'theme:', theme, 'resolvedTheme:', resolvedTheme);
|
||||
}, [mounted, theme, resolvedTheme]);
|
||||
|
||||
const toggleTheme = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const isDark = resolvedTheme === "dark";
|
||||
|
||||
// Fallback for browsers without startViewTransition
|
||||
if (!document.startViewTransition) {
|
||||
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
// Skip animation if user prefers reduced motion or browser doesn't support view transitions
|
||||
if (prefersReducedMotion || !document.startViewTransition) {
|
||||
setTheme(isDark ? "light" : "dark");
|
||||
return;
|
||||
}
|
||||
@@ -51,18 +57,21 @@ export function ThemeToggle() {
|
||||
});
|
||||
};
|
||||
|
||||
const isDark = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={mounted ? toggleTheme : undefined}
|
||||
className="rounded-full"
|
||||
aria-label="Toggle theme"
|
||||
aria-label={mounted ? (isDark ? "Switch to light mode" : "Switch to dark mode") : "Toggle theme"}
|
||||
>
|
||||
{mounted && resolvedTheme === "dark" ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
{/* Show the icon representing what clicking will switch TO */}
|
||||
{mounted && isDark ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user