mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-19 05:48:33 +02:00
feat: Initial Monorepo Setup for Such Software
- **Architecture**: Initialized Turborepo monorepo with pnpm workspace. - **Frontend**: Scaffolding for Next.js 14 app (apps/web) with Tailwind CSS v4 configuration. - **UI System**: Created shared UI package (@repo/ui) with Shadcn/UI primitives (Card, Button). - **Procedural Design**: - Implemented 'NebulaField' organic background animation for Home. - Implemented 'DataGrid' isometric background for Services. - Added 'DynamicBackground' for theme-aware transitions. - **Features**: - Responsive 'ThemeToggle' with View Transitions API support. - 'PortfolioGrid' component showcasing key projects (r/CryptoCurrency, etc.). - 'FounderSignature' component detailing accessibility focus. - 'ContactForm' with Zod validation + turnstile integration stub. - **Pages**: - Landing page with parallax/procedural visuals. - Services page highlighting Medusa v2 development capabilities. - **Clean Up**: Removed initial Medusa backend scaffold to focus on frontend delivery first.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { Card } from "@repo/ui/components/card";
|
||||
|
||||
export const FounderSignature = () => {
|
||||
return (
|
||||
<section className="w-full max-w-4xl mx-auto py-20 px-4">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Accessibility as Architecture</h2>
|
||||
<div className="prose dark:prose-invert">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
"During my three years at <span className="text-foreground font-semibold">Cecil College</span>, I didn't just teach web design—I re-engineered how it was taught.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
||||
Transitioning the curriculum toward 'Digital Arts and Media Design,' I embedded inclusive design principles into the foundation of every course. I taught students that a website isn't truly 'live' until it is accessible to everyone.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
||||
This wasn't just academic theory; it was a rigorous discipline of semantic HTML, ARIA compliance, and cognitive load management. Today, I bring that same academic rigor to Such Software’s commercial projects. We don't retroactively fix accessibility; we architect for it from line one."
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-border">
|
||||
<p className="font-mono text-sm uppercase tracking-widest text-primary">Founder & Principle Engineer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="relative p-8 bg-muted/30 border-dashed">
|
||||
<div className="absolute -top-4 -right-4 w-24 h-24 bg-blue-500/10 rounded-full blur-2xl" />
|
||||
<div className="absolute -bottom-4 -left-4 w-32 h-32 bg-purple-500/10 rounded-full blur-2xl" />
|
||||
|
||||
<h3 className="tex-lg font-semibold mb-4">Core Competencies</h3>
|
||||
<ul className="space-y-3 font-mono text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span> WCAG 2.2 AA Compliance
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span> Section 508 & ADA
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span> Semantic HTML5 Architecture
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-500">✓</span> Playwright A11y Automation
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
import { useFormState, useFormStatus } from 'react-dom'; // Note: check react-dom version for useFormState support or import from hook
|
||||
// Next.js 14 uses react-dom for these
|
||||
import { submitContactForm } from '@/actions/submit-contact';
|
||||
import { TurnstileWidget } from './turnstile-widget';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@repo/ui/components/button';
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending ? "Sending..." : "Send Message"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContactForm() {
|
||||
const [state, formAction] = useFormState(submitContactForm, { success: false, message: '' });
|
||||
const [token, setToken] = useState<string>('');
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-4 max-w-md mx-auto p-4 border rounded-lg bg-card text-card-foreground">
|
||||
<h3 className="text-xl font-bold">Contact Us</h3>
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium">Name</label>
|
||||
<input name="name" id="name" required className="w-full mt-1 p-2 border rounded bg-background" />
|
||||
{state?.errors?.name && <p className="text-red-500 text-xs">{state.errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium">Email</label>
|
||||
<input name="email" id="email" type="email" required className="w-full mt-1 p-2 border rounded bg-background" />
|
||||
{state?.errors?.email && <p className="text-red-500 text-xs">{state.errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium">Message</label>
|
||||
<textarea name="message" id="message" required className="w-full mt-1 p-2 border rounded bg-background h-32" />
|
||||
{state?.errors?.message && <p className="text-red-500 text-xs">{state.errors.message}</p>}
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="token" value={token} />
|
||||
|
||||
<div className="my-4">
|
||||
<TurnstileWidget onVerify={setToken} />
|
||||
</div>
|
||||
|
||||
<SubmitButton />
|
||||
|
||||
{state?.message && (
|
||||
<p className={`text-sm ${state.success ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
|
||||
export function TurnstileWidget({ onVerify }: { onVerify: (token: string) => void }) {
|
||||
return (
|
||||
<Turnstile
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
|
||||
onSuccess={onVerify}
|
||||
options={{ theme: 'auto' }} // Matches our Light/Dark mode!
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { NebulaField } from "@repo/ui/procedural/nebula-field";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function DynamicBackground() {
|
||||
const { theme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<NebulaField
|
||||
className="fixed inset-0 z-0 h-screen w-screen"
|
||||
themeMode={theme === 'dark' ? 'dark' : 'light'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Card } from "@repo/ui/components/card";
|
||||
import Image from "next/image";
|
||||
|
||||
export const PortfolioGrid = () => {
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-4 max-w-7xl mx-auto p-4 z-10 relative">
|
||||
{/* Hero Project: r/CryptoCurrency */}
|
||||
<Card className="md:col-span-2 md:row-span-2 relative group overflow-hidden border-0 bg-slate-900">
|
||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/80 to-transparent p-6 flex flex-col justify-end">
|
||||
<h3 className="text-3xl font-bold text-white">r/CryptoCurrency</h3>
|
||||
<p className="text-gray-200">Governance dashboard for 10M+ members. Real-time DAO metrics.</p>
|
||||
</div>
|
||||
{/* Placeholder image since we don't have the assets yet */}
|
||||
<div className="absolute inset-0 bg-slate-800" />
|
||||
</Card>
|
||||
|
||||
{/* Secondary: Occupy Wallets */}
|
||||
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100">
|
||||
<div className="absolute inset-0 z-10 p-6 flex flex-col justify-center items-center text-center">
|
||||
<h3 className="text-2xl font-bold text-zinc-900">Occupy Wallets</h3>
|
||||
<p className="text-zinc-600">High-fidelity gallery for fiscal graffiti.</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
||||
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/90 dark:bg-orange-950/90 border-orange-500/20">
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
|
||||
<ul className="mt-2 space-y-2 text-sm">
|
||||
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-50">(Game)</span></li>
|
||||
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-50">(WASM)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quaternary: MoonPlace */}
|
||||
<Card className="md:col-span-1 relative group bg-indigo-50/90 dark:bg-indigo-950/90 border-indigo-500/20">
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
|
||||
<p className="text-sm mt-2">Collaborative pixel canvas engine.</p>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const toggleTheme = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const isDark = theme === "dark";
|
||||
|
||||
// Fallback for browsers without startViewTransition
|
||||
if (!document.startViewTransition) {
|
||||
setTheme(isDark ? "light" : "dark");
|
||||
return;
|
||||
}
|
||||
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
const endRadius = Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y)
|
||||
);
|
||||
|
||||
const transition = document.startViewTransition(() => {
|
||||
setTheme(isDark ? "light" : "dark");
|
||||
});
|
||||
|
||||
transition.ready.then(() => {
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||
];
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: isDark ? [...clipPath].reverse() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in-out",
|
||||
pseudoElement: isDark ? "::view-transition-old(root)" : "::view-transition-new(root)",
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="ghost" size="icon" onClick={toggleTheme} className="rounded-full">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user