mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-19 05:48:33 +02:00
Add splash intro, nuclear games + consulting, Cherenkov hero
- Splash: full-viewport animated SUCH logo cover with four corner nav buttons; reveals the site once "entered" (scroll-through or Explore) and does not resurrect on scroll-up. Original hero restored beneath it (title-anchored monitor pulse + Cherenkov-blue background). - Logo: traveling Cherenkov glow ring + gentle letter breath (SVG/Framer, reduced-motion safe); beveled glass corner buttons. - Services: new "Nuclear & Radiation Modeling" section + "Consulting" umbrella (payments, crypto, nuclear), MCNP Google Scholar link, and a not-a-licensed-PE clarification. - About/founder bio: LLNL credibility + accessibility, art101.io link. - Products: add Barns & Neutrons, Alpha Doc, Decay Theory (detail pages, OG cards, SVG icons); client-side sortable grid with coming-soon at the bottom. New Neroswap + Custom Websites icons (de-duplicated). - SEO: game pages added to sitemap. - Copy: dial back em-dashes to colons/commas/parens on home + services.
This commit is contained in:
@@ -38,10 +38,10 @@ export const FounderSignature = () => {
|
||||
"Accessibility isn't bolted on. It's there from the first line of code."
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-6">
|
||||
John Winter Murphy holds a <span className="text-foreground font-semibold">PhD in Materials Engineering from UT-Dallas (2014)</span>, focused on semiconductor device fabrication for radiation detectors. From <span className="text-foreground font-semibold">2015 to 2021 he was a scientist at Lawrence Livermore National Laboratory</span>, developing radiation detector systems and the lab's nuclear battery program.
|
||||
John Winter Murphy holds a <span className="text-foreground font-semibold">PhD in Materials Engineering from UT-Dallas (2014)</span>, focused on semiconductor devices for radiation detection. From <span className="text-foreground font-semibold">2015 to 2021 he was a scientist at Lawrence Livermore National Laboratory</span>, developing radiation detector systems and the lab's radioisotope battery program. He brings that same lab-bench rigor to software, and an insistence that it stay <span className="text-foreground font-semibold">accessible to everyone</span>, audited to WCAG 2.2.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
||||
He previously co-founded <span className="text-foreground font-semibold">Patrn LLC</span> (art101.io), a series of art-history NFT drops, and <span className="text-foreground font-semibold">Tabby Labs</span>, which was developing a cryptocurrency wallet with an interactive world inside.
|
||||
He previously co-founded <span className="text-foreground font-semibold">Patrn LLC</span> (<a href="https://art101.io" target="_blank" rel="noopener noreferrer" className="text-foreground font-semibold underline decoration-primary/50 hover:text-primary transition-colors">art101.io</a>), a series of art-history NFT drops, and <span className="text-foreground font-semibold">Tabby Labs</span>, which was developing a cryptocurrency wallet with an interactive world inside.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
||||
He most recently taught engineering at <span className="text-foreground font-semibold">Cecil College</span> and senior-level materials engineering (microfabrication, nanomaterials) at <span className="text-foreground font-semibold">Frostburg State University</span>. Active in cryptocurrency since 2014, with a public track record on <a href="https://github.com/jwinterm" target="_blank" rel="noopener noreferrer" className="text-foreground font-semibold underline decoration-primary/50 hover:text-primary transition-colors">GitHub</a>.
|
||||
|
||||
@@ -15,9 +15,11 @@ const NebulaField = dynamic(
|
||||
|
||||
interface DynamicBackgroundProps {
|
||||
nebulaPosition?: { x: number; y: number } | null;
|
||||
/** When false, the nebula stays hidden (e.g. while the home splash is active). */
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export function DynamicBackground({ nebulaPosition }: DynamicBackgroundProps) {
|
||||
export function DynamicBackground({ nebulaPosition, visible = true }: DynamicBackgroundProps) {
|
||||
const { resolvedTheme, systemTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [showNebula, setShowNebula] = useState(false);
|
||||
@@ -54,15 +56,19 @@ export function DynamicBackground({ nebulaPosition }: DynamicBackgroundProps) {
|
||||
const effectiveTheme = resolvedTheme || systemTheme || 'dark';
|
||||
const themeMode = effectiveTheme === 'dark' ? 'dark' : 'light';
|
||||
|
||||
if (!showNebula) {
|
||||
// While the splash is active (visible === false) keep the nebula/monitor hidden —
|
||||
// the splash provides its own Cherenkov glow. Reveal it once the visitor enters.
|
||||
if (!showNebula || !visible) {
|
||||
return <div className="fixed inset-0 z-0 h-screen w-screen bg-background" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NebulaField
|
||||
className="fixed inset-0 z-0 h-screen w-screen"
|
||||
themeMode={themeMode}
|
||||
position={nebulaPosition ?? undefined}
|
||||
/>
|
||||
<div className="fixed inset-0 z-0 h-screen w-screen animate-in fade-in duration-700">
|
||||
<NebulaField
|
||||
className="absolute inset-0 h-full w-full"
|
||||
themeMode={themeMode}
|
||||
position={nebulaPosition ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
const navButtons = [
|
||||
{ label: "Portfolio", href: "/products", corner: "left-4 top-16 sm:left-6 sm:top-24 lg:left-8 lg:top-32" },
|
||||
{ label: "Apps", href: "/apps", corner: "right-4 top-16 sm:right-6 sm:top-24 lg:right-8 lg:top-32" },
|
||||
{ label: "Services", href: "/services", corner: "left-4 bottom-24 sm:left-6 lg:left-8 lg:bottom-32" },
|
||||
{ label: "About", href: "/about", corner: "right-4 bottom-24 sm:right-6 lg:right-8 lg:bottom-32" },
|
||||
];
|
||||
|
||||
/** The "SUCH" glyph mark (2x2 letter grid) with a Cherenkov-blue treatment. */
|
||||
function SuchMark({ animate }: { animate: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 200 200"
|
||||
role="img"
|
||||
aria-label="Such Software"
|
||||
className="h-auto w-[min(74vw,44vh)] drop-shadow-[0_0_40px_rgba(34,211,238,0.25)]"
|
||||
>
|
||||
<defs>
|
||||
<filter id="splash-glow" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="1.6" result="b" />
|
||||
<feMerge>
|
||||
<feMergeNode in="b" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
{/* steady bounding box, Cherenkov cyan */}
|
||||
<rect
|
||||
x="14" y="14" width="172" height="172" rx="16"
|
||||
fill="none" stroke="#22d3ee" strokeWidth="3.5" filter="url(#splash-glow)" opacity="0.45"
|
||||
/>
|
||||
{/* a Cherenkov glow segment slowly circling the ring */}
|
||||
{animate && (
|
||||
<motion.rect
|
||||
x="14" y="14" width="172" height="172" rx="16"
|
||||
fill="none" stroke="#a5f3fc" strokeWidth="4" strokeLinecap="round" filter="url(#splash-glow)"
|
||||
pathLength={1} strokeDasharray="0.16 0.84"
|
||||
initial={{ strokeDashoffset: 0 }}
|
||||
animate={{ strokeDashoffset: [0, -1] }}
|
||||
transition={{ duration: 5, ease: "linear", repeat: Infinity }}
|
||||
/>
|
||||
)}
|
||||
{/* letters with a gentle breath */}
|
||||
<motion.g
|
||||
animate={animate ? { opacity: [0.9, 1, 0.9] } : undefined}
|
||||
transition={animate ? { duration: 5, ease: "easeInOut", repeat: Infinity } : undefined}
|
||||
>
|
||||
{/* S */}
|
||||
<path d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#35c98e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* U */}
|
||||
<path d="m 118,36 v 34 c 0,12.15 9.85,14 22,14 12.15,0 22,-1.85 22,-14 V 36" fill="none" stroke="#e8689e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* C */}
|
||||
<path d="M 78,118 H 36 V 164 H 78" fill="none" stroke="#4aade0" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* H */}
|
||||
<path d="M 118,118 V 164 M 162,118 V 164 M 118,141 H 162" fill="none" stroke="#45c99e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</motion.g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; sectionRef?: any }) {
|
||||
const prefersReduced = useReducedMotion();
|
||||
const animate = !prefersReduced;
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
aria-label="Such Software"
|
||||
className="relative z-10 flex min-h-[100svh] w-full max-w-7xl flex-col items-center justify-center px-4 py-12 text-center"
|
||||
>
|
||||
{/* Logo + pulsing Cherenkov glow */}
|
||||
<div className="relative flex items-center justify-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute h-[120%] w-[120%] rounded-full bg-[radial-gradient(circle,rgba(34,211,238,0.18),transparent_65%)] motion-safe:animate-pulse"
|
||||
style={{ animationDuration: "5s" }}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.94 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
<SuchMark animate={animate} />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Cover tagline (the real page H1 lives in the hero revealed after entering) */}
|
||||
<p className="mt-10 bg-gradient-to-r from-cyan-400 to-blue-600 bg-clip-text pb-1 text-3xl font-bold text-transparent sm:text-4xl md:text-5xl">
|
||||
Precision Engineering for Everyone
|
||||
</p>
|
||||
<p className="mt-4 max-w-xl text-base text-muted-foreground sm:text-lg">
|
||||
A software studio in Kennett Square, PA. Custom e-commerce, apps, games,
|
||||
payments, and consulting.
|
||||
</p>
|
||||
|
||||
{/* Nav buttons in the four corners (all breakpoints) */}
|
||||
{navButtons.map((b) => (
|
||||
<Link
|
||||
key={b.href}
|
||||
href={b.href}
|
||||
className={`absolute flex items-center justify-center rounded-2xl border border-cyan-400/25 bg-foreground/[0.06] backdrop-blur-md shadow-md shadow-black/20 ring-1 ring-inset ring-white/10 px-4 py-3 text-sm font-semibold transition-all hover:scale-[1.03] hover:border-cyan-400/60 hover:bg-foreground/10 hover:text-cyan-600 dark:hover:text-cyan-400 hover:shadow-lg hover:shadow-cyan-500/15 w-28 sm:w-36 sm:px-6 sm:py-4 sm:text-base lg:w-48 ${b.corner}`}
|
||||
>
|
||||
{b.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Enter-the-site affordance */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEnter}
|
||||
className="absolute bottom-6 left-1/2 flex -translate-x-1/2 flex-col items-center gap-1 text-xs uppercase tracking-[0.3em] text-muted-foreground transition-colors hover:text-foreground focus-visible:text-foreground"
|
||||
>
|
||||
<span>Explore</span>
|
||||
<ChevronDown className="h-5 w-5 motion-safe:animate-bounce" aria-hidden="true" />
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,8 @@ export function ServicesPreview() {
|
||||
<div className="text-left mb-8 section-container border-l-4 border-l-emerald-500">
|
||||
<h2 className="text-3xl font-bold mb-3 tracking-tight">What We Do</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||
We build high-performance e-commerce stores, develop custom apps, and ship our own titles.
|
||||
We build high-performance e-commerce stores, develop custom apps, and ship our own
|
||||
titles, engineered with atomic precision by a studio that sweats the details.
|
||||
Whether you need a storefront, a bespoke tool, or a full product build, we handle the engineering end to end.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,20 @@ import { usePathname } from "next/navigation";
|
||||
import { SiteLogo } from "./site-logo";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
export function Header() {
|
||||
/**
|
||||
* On the home page the splash owns the first screen, so the header is `floating`
|
||||
* (fixed) and only shown once the visitor has `visible`-entered the site. Other
|
||||
* pages use the default always-on sticky header.
|
||||
*/
|
||||
export function Header({ floating = false, visible = true }: { floating?: boolean; visible?: boolean }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const positionClasses = floating
|
||||
? `fixed top-0 inset-x-0 transition-all duration-500 ${
|
||||
visible ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0 invisible"
|
||||
}`
|
||||
: "sticky top-0";
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
@@ -16,7 +27,7 @@ export function Header() {
|
||||
>
|
||||
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">
|
||||
<header className={`z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl rounded-b-3xl border-b border-border/50 ${positionClasses}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<SiteLogo />
|
||||
<nav className="hidden md:flex gap-6 text-sm font-medium" aria-label="Main navigation">
|
||||
|
||||
@@ -12,7 +12,11 @@ const navItems = [
|
||||
{ href: "/about", label: "About", icon: FaCircleInfo },
|
||||
];
|
||||
|
||||
export function MobileNav() {
|
||||
/**
|
||||
* `visible` is driven by the home-page "entered" state so the bottom bar stays
|
||||
* hidden under the splash. Defaults to visible everywhere else.
|
||||
*/
|
||||
export function MobileNav({ visible = true }: { visible?: boolean }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive = (href: string) => {
|
||||
@@ -20,9 +24,13 @@ export function MobileNav() {
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
const visibilityClasses = visible
|
||||
? "translate-y-0 opacity-100"
|
||||
: "translate-y-full opacity-0 invisible";
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-50 md:hidden bg-background/95 backdrop-blur-xl border-t border-border/50"
|
||||
className={`fixed bottom-0 left-0 right-0 z-50 md:hidden bg-background/95 backdrop-blur-xl border-t border-border/50 transition-all duration-300 ${visibilityClasses}`}
|
||||
aria-label="Mobile navigation"
|
||||
style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||
import { Card } from "@repo/ui/components/card";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export type Product = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
icon?: string;
|
||||
/** ISO date string. Null for ongoing services or unreleased items. */
|
||||
releaseDate?: string | null;
|
||||
/** Not yet released. Sorts as "now" by release date and renders without a detail link. */
|
||||
comingSoon?: boolean;
|
||||
};
|
||||
|
||||
const colorClasses: Record<string, string> = {
|
||||
emerald: "border-emerald-500/30 hover:border-emerald-500/60",
|
||||
purple: "border-purple-500/30 hover:border-purple-500/60",
|
||||
amber: "border-amber-500/30 hover:border-amber-500/60",
|
||||
blue: "border-blue-500/30 hover:border-blue-500/60",
|
||||
pink: "border-pink-500/30 hover:border-pink-500/60",
|
||||
orange: "border-orange-500/30 hover:border-orange-500/60",
|
||||
yellow: "border-yellow-500/30 hover:border-yellow-500/60",
|
||||
green: "border-green-500/30 hover:border-green-500/60",
|
||||
cyan: "border-cyan-400/30 hover:border-cyan-400/60",
|
||||
};
|
||||
|
||||
const titleColors: Record<string, string> = {
|
||||
emerald: "text-emerald-600 dark:text-emerald-400",
|
||||
purple: "text-purple-600 dark:text-purple-400",
|
||||
amber: "text-amber-600 dark:text-amber-400",
|
||||
blue: "text-blue-600 dark:text-blue-400",
|
||||
pink: "text-pink-600 dark:text-pink-400",
|
||||
orange: "text-orange-600 dark:text-orange-400",
|
||||
yellow: "text-yellow-600 dark:text-yellow-400",
|
||||
green: "text-green-600 dark:text-green-400",
|
||||
cyan: "text-cyan-600 dark:text-cyan-400",
|
||||
};
|
||||
|
||||
const glyphBg: Record<string, string> = {
|
||||
emerald: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
|
||||
purple: "bg-purple-500/15 text-purple-600 dark:text-purple-400",
|
||||
amber: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
|
||||
blue: "bg-blue-500/15 text-blue-600 dark:text-blue-400",
|
||||
pink: "bg-pink-500/15 text-pink-600 dark:text-pink-400",
|
||||
orange: "bg-orange-500/15 text-orange-600 dark:text-orange-400",
|
||||
yellow: "bg-yellow-500/15 text-yellow-600 dark:text-yellow-400",
|
||||
green: "bg-green-500/15 text-green-600 dark:text-green-400",
|
||||
cyan: "bg-cyan-400/15 text-cyan-600 dark:text-cyan-400",
|
||||
};
|
||||
|
||||
type SortKey = "release" | "name";
|
||||
|
||||
function effectiveTime(p: Product): number {
|
||||
if (p.releaseDate) return Date.parse(p.releaseDate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function ProductIcon({ product }: { product: Product }) {
|
||||
if (product.icon) {
|
||||
return (
|
||||
<Image
|
||||
src={product.icon}
|
||||
alt={`${product.title} icon`}
|
||||
width={56}
|
||||
height={56}
|
||||
className="rounded-xl flex-shrink-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-xl text-2xl font-bold ${glyphBg[product.color] ?? glyphBg.cyan}`}
|
||||
>
|
||||
{product.title.charAt(0)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProductCardInner({ product }: { product: Product }) {
|
||||
return (
|
||||
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all`}>
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<ProductIcon product={product} />
|
||||
<div className="flex flex-col">
|
||||
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
|
||||
{product.title}
|
||||
</h2>
|
||||
{product.comingSoon && (
|
||||
<span className="mt-1 inline-flex w-fit items-center rounded-full bg-cyan-400/15 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-600 dark:text-cyan-400">
|
||||
Coming Soon
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground leading-relaxed">{product.description}</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductsGrid({ products }: { products: Product[] }) {
|
||||
const [sort, setSort] = useState<SortKey>("release");
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const copy = [...products];
|
||||
if (sort === "name") {
|
||||
copy.sort((a, b) => a.title.localeCompare(b.title));
|
||||
} else {
|
||||
// Released work first (newest first); coming-soon items grouped at the bottom.
|
||||
copy.sort((a, b) => {
|
||||
const aSoon = !!a.comingSoon;
|
||||
const bSoon = !!b.comingSoon;
|
||||
if (aSoon !== bSoon) return aSoon ? 1 : -1;
|
||||
return effectiveTime(b) - effectiveTime(a);
|
||||
});
|
||||
}
|
||||
return copy;
|
||||
}, [products, sort]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8 flex items-center justify-end gap-3">
|
||||
<label htmlFor="product-sort" className="text-sm text-muted-foreground">
|
||||
Sort by
|
||||
</label>
|
||||
<select
|
||||
id="product-sort"
|
||||
value={sort}
|
||||
onChange={(e) => setSort(e.target.value as SortKey)}
|
||||
className="rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-cyan-400/50"
|
||||
>
|
||||
<option value="release">Release date (newest)</option>
|
||||
<option value="name">Name (A–Z)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{sorted.map((product, i) => (
|
||||
<ScrollReveal key={product.slug} delay={(i % 2) * 0.08}>
|
||||
<Link href={`/products/${product.slug}`} className="block h-full">
|
||||
<div className="h-full cursor-pointer">
|
||||
<ProductCardInner product={product} />
|
||||
</div>
|
||||
</Link>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user