Fix critical accessibility and SEO issues
- Fix homepage heading hierarchy (h2 -> h1) - Add page-specific metadata to services and about pages - Fix outline-none on form inputs for keyboard accessibility - Delete sync conflict files - Add group class back to bento cards for hover animations - Use raw CSS for glassmorphism to fix Tailwind opacity errors - Add TODO.md with production checklist - Add semantic CSS classes (.section-container, .glass-card) - Update Tailwind config for opacity modifier support
@@ -0,0 +1,131 @@
|
|||||||
|
# Such Software - Production Checklist
|
||||||
|
|
||||||
|
## 🔴 Before Production Deployment
|
||||||
|
|
||||||
|
### 1. Cloudflare Turnstile Configuration
|
||||||
|
|
||||||
|
The contact form uses Cloudflare Turnstile for bot protection. You need to set up the secret key for production:
|
||||||
|
|
||||||
|
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → Turnstile
|
||||||
|
2. Create a new site or use existing widget
|
||||||
|
3. Copy the **Site Key** (public) and **Secret Key** (private)
|
||||||
|
4. Set environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your production environment (Vercel, etc.)
|
||||||
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
|
||||||
|
TURNSTILE_SECRET_KEY=your_secret_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Warning:** The current implementation returns `true` if no secret key is set. This is fine for development but must be configured in production.
|
||||||
|
|
||||||
|
See: `apps/web/src/lib/turnstile.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Medium Priority Improvements
|
||||||
|
|
||||||
|
### 2. Extract Shared Header Component
|
||||||
|
|
||||||
|
The header is duplicated across three pages:
|
||||||
|
- `apps/web/src/app/page.tsx`
|
||||||
|
- `apps/web/src/app/services/page.tsx`
|
||||||
|
- `apps/web/src/app/about/page.tsx`
|
||||||
|
|
||||||
|
Create a shared component at `apps/web/src/components/layout/header.tsx`.
|
||||||
|
|
||||||
|
### 3. Add `prefers-reduced-motion` Support
|
||||||
|
|
||||||
|
The NebulaField and DynamicBackground components run animations without checking user preferences.
|
||||||
|
|
||||||
|
File: `packages/ui/src/procedural/nebula-field.tsx`
|
||||||
|
|
||||||
|
Add:
|
||||||
|
```tsx
|
||||||
|
const prefersReducedMotion = typeof window !== 'undefined'
|
||||||
|
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (prefersReducedMotion) {
|
||||||
|
return <div className="absolute inset-0 bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800" />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Add Skip-to-Content Link
|
||||||
|
|
||||||
|
For keyboard accessibility, add a skip link in `apps/web/src/app/layout.tsx`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg">
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Add `aria-hidden` to Decorative Icons
|
||||||
|
|
||||||
|
In `apps/web/src/app/about/page.tsx`, add `aria-hidden="true"` to decorative icons:
|
||||||
|
```tsx
|
||||||
|
<FaMapMarkerAlt aria-hidden="true" />
|
||||||
|
<FaPhone aria-hidden="true" />
|
||||||
|
<FaBuilding aria-hidden="true" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Type Server Action Properly
|
||||||
|
|
||||||
|
In `apps/web/src/actions/submit-contact.ts`, the `prevState` parameter uses `any`. Update to:
|
||||||
|
```tsx
|
||||||
|
export async function submitContactForm(
|
||||||
|
prevState: { success: boolean; message: string; errors?: Record<string, string> },
|
||||||
|
formData: FormData
|
||||||
|
): Promise<{ success: boolean; message: string; errors?: Record<string, string> }>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Rate Limiting
|
||||||
|
|
||||||
|
Consider adding rate limiting to the contact form submission to prevent abuse.
|
||||||
|
Options: Upstash Ratelimit, Vercel Edge Config, or Cloudflare Workers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Nice-to-Have
|
||||||
|
|
||||||
|
### 8. Add Structured Data (JSON-LD)
|
||||||
|
|
||||||
|
Add Organization schema to `layout.tsx` for better SEO:
|
||||||
|
```tsx
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Such Software LLC",
|
||||||
|
"url": "https://such.software",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "110 E State St, Suite 300",
|
||||||
|
"addressLocality": "Kennett Square",
|
||||||
|
"addressRegion": "PA",
|
||||||
|
"postalCode": "19348",
|
||||||
|
"addressCountry": "US"
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Add Canonical URLs
|
||||||
|
|
||||||
|
Add canonical meta tags to prevent duplicate content issues.
|
||||||
|
|
||||||
|
### 10. Remove Console Logs
|
||||||
|
|
||||||
|
Remove `console.log` statements before production:
|
||||||
|
- `apps/web/src/components/contact/turnstile-widget.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Recently Completed
|
||||||
|
|
||||||
|
- [x] Deleted sync conflict files
|
||||||
|
- [x] Fixed homepage `<h1>` heading hierarchy
|
||||||
|
- [x] Added page metadata to services and about pages
|
||||||
|
- [x] Fixed `outline-none` accessibility issue on form inputs
|
||||||
|
- [x] Established semantic CSS architecture (`.section-container`, `.glass-card`)
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "Such Software",
|
||||||
|
"short_name": "Such Software",
|
||||||
|
"description": "Next-generation web applications engineered for scale, compliance, and performance.",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/images/favicon_192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,17 +1,29 @@
|
|||||||
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { FounderSignature } from "@/components/about/founder-signature";
|
import { FounderSignature } from "@/components/about/founder-signature";
|
||||||
|
import { SiteLogo } from "@/components/layout/site-logo";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
|
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "About | Such Software",
|
||||||
|
description: "Full-service software provider in Kennett Square, PA. Founded by John Winter Murphy, specializing in accessible web engineering and WCAG 2.2 compliance.",
|
||||||
|
openGraph: {
|
||||||
|
title: "About | Such Software",
|
||||||
|
description: "Full-service software provider in Kennett Square, PA. Founded by John Winter Murphy.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
return (
|
return (
|
||||||
<main className="relative flex flex-col items-center overflow-hidden">
|
<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-sm sticky top-0">
|
<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-6">
|
<div className="flex items-center gap-8">
|
||||||
<Link href="/" className="text-2xl font-bold tracking-tighter">Such Software</Link>
|
<SiteLogo />
|
||||||
<nav className="hidden md:flex gap-4 text-sm font-medium">
|
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||||
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
||||||
<Link href="/about" className="text-primary transition-colors">About</Link>
|
<Link href="/about" className="text-primary transition-colors">About</Link>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -19,10 +31,10 @@ export default function AboutPage() {
|
|||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="z-10 w-full max-w-4xl mx-auto py-20 px-4">
|
<div 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>
|
<h1 className="text-4xl md:text-5xl font-bold mb-10 text-center">About Us</h1>
|
||||||
|
|
||||||
<div className="bg-card/80 backdrop-blur-md rounded-2xl p-8 md:p-12 shadow-xl border border-border/50">
|
<div className="section-container">
|
||||||
|
|
||||||
<div className="prose dark:prose-invert max-w-none mb-12">
|
<div className="prose dark:prose-invert max-w-none mb-12">
|
||||||
<p className="text-xl leading-relaxed">
|
<p className="text-xl leading-relaxed">
|
||||||
|
|||||||
@@ -74,3 +74,29 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@layer components {
|
||||||
|
/* High-contrast content masking for accessibility and streamer protection */
|
||||||
|
.mask-backdrop {
|
||||||
|
background-color: hsl(var(--card) / 0.98);
|
||||||
|
backdrop-filter: blur(64px);
|
||||||
|
-webkit-backdrop-filter: blur(64px);
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: hsl(var(--border) / 0.5);
|
||||||
|
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container {
|
||||||
|
@apply mask-backdrop rounded-[2.5rem] p-10 md:p-16 text-card-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
@apply mask-backdrop rounded-[2rem] p-8 text-card-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card:hover {
|
||||||
|
border-color: hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,20 @@ import { DynamicBackground } from "@/components/landing/dynamic-background";
|
|||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
// ... (metadata omitted)
|
export const metadata: Metadata = {
|
||||||
|
title: "Such Software | Digital Craftsmanship",
|
||||||
|
description: "Next-generation web applications engineered for scale, compliance, and performance.",
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
|
||||||
|
],
|
||||||
|
shortcut: "/images/favicon_192.png",
|
||||||
|
apple: [
|
||||||
|
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
manifest: "/manifest.json",
|
||||||
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PortfolioGrid } from "@/components/portfolio/bento-grid";
|
|||||||
import { ContactForm } from "@/components/contact/contact-form";
|
import { ContactForm } from "@/components/contact/contact-form";
|
||||||
import { FounderSignature } from "@/components/about/founder-signature";
|
import { FounderSignature } from "@/components/about/founder-signature";
|
||||||
import { ServicesPreview } from "@/components/landing/services-preview";
|
import { ServicesPreview } from "@/components/landing/services-preview";
|
||||||
|
import { SiteLogo } from "@/components/layout/site-logo";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@repo/ui/components/button";
|
import { Button } from "@repo/ui/components/button";
|
||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
@@ -12,10 +13,10 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<main className="relative flex flex-col items-center justify-between overflow-hidden">
|
<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 pb-2">
|
<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-6">
|
<div className="flex items-center gap-8">
|
||||||
<h1 className="text-2xl font-bold tracking-tighter">Such Software</h1>
|
<SiteLogo />
|
||||||
<nav className="hidden md:flex gap-4 text-sm font-medium">
|
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||||
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
||||||
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -24,9 +25,9 @@ export default function Home() {
|
|||||||
</header>
|
</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">
|
<div className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
|
||||||
<h2 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">
|
<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
|
Digital Craftsmanship
|
||||||
</h2>
|
</h1>
|
||||||
<p className="max-w-2xl text-xl text-muted-foreground mb-8">
|
<p className="max-w-2xl text-xl text-muted-foreground mb-8">
|
||||||
Next-generation web applications engineered for scale, compliance, and performance.
|
Next-generation web applications engineered for scale, compliance, and performance.
|
||||||
</p>
|
</p>
|
||||||
@@ -47,9 +48,9 @@ export default function Home() {
|
|||||||
<ServicesPreview />
|
<ServicesPreview />
|
||||||
|
|
||||||
<div className="w-full max-w-6xl mx-auto mb-16 px-4">
|
<div className="w-full max-w-6xl mx-auto mb-16 px-4">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-blue-500/50 pl-6">
|
<div className="text-left mb-8 px-2 border-l-2 border-blue-500/50 section-container">
|
||||||
<h3 className="text-2xl font-bold mb-2">Selected Works</h3>
|
<h3 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Selected Works</h3>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
A curated gallery of recent projects where we've pushed the boundaries of
|
A curated gallery of recent projects where we've pushed the boundaries of
|
||||||
performance, accessibility, and user experience.
|
performance, accessibility, and user experience.
|
||||||
</p>
|
</p>
|
||||||
@@ -57,42 +58,21 @@ export default function Home() {
|
|||||||
<PortfolioGrid />
|
<PortfolioGrid />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full max-w-6xl mx-auto mb-16 px-4">
|
<div className="w-full max-w-6xl mx-auto mb-20 px-4">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-purple-500/50 pl-6">
|
<div className="text-left mb-12 px-2 border-l-2 border-purple-500/50 section-container">
|
||||||
<h3 className="text-2xl font-bold mb-2">Inclusive by Design</h3>
|
<h3 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Inclusive by Design</h3>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
We believe the web is for everyone. Every line of code we write is
|
We believe the web is for everyone. Every line of code we write is
|
||||||
audited for WCAG 2.1 compliance and high-performance across all devices.
|
audited for WCAG 2.2 compliance and high-performance across all devices.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid md:grid-cols-2 gap-8">
|
<FounderSignature />
|
||||||
<Card className="p-8 bg-card/50 backdrop-blur-sm border-purple-500/10 hover:border-purple-500/30 transition-all group">
|
|
||||||
<h4 className="text-xl font-bold mb-4 flex items-center gap-2">
|
|
||||||
<span className="w-8 h-8 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500 text-sm">01</span>
|
|
||||||
A11y First Architecture
|
|
||||||
</h4>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Semantic HTML, ARIA patterns, and keyboard navigation aren't afterthoughts—they are the foundation of our build process.
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-8 bg-card/50 backdrop-blur-sm border-blue-500/10 hover:border-blue-500/30 transition-all group">
|
|
||||||
<h4 className="text-xl font-bold mb-4 flex items-center gap-2">
|
|
||||||
<span className="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500 text-sm">02</span>
|
|
||||||
Edge Performance
|
|
||||||
</h4>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
By leveraging edge computing and static generation, we ensure your site is usable even on 3G connections and older hardware.
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FounderSignature />
|
|
||||||
|
|
||||||
<div id="contact" className="w-full mt-20 mb-20 flex flex-col items-center">
|
<div id="contact" className="w-full mt-20 mb-20 flex flex-col items-center">
|
||||||
<div className="w-full max-w-6xl text-left mb-12 px-2 border-l-2 border-emerald-500/50 pl-6">
|
<div className="w-full max-w-6xl text-left mb-12 px-2 border-l-2 border-emerald-500/50 section-container">
|
||||||
<h3 className="text-2xl font-bold mb-2">Start a Project</h3>
|
<h3 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Start a Project</h3>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
Ready to build something exceptional? Drop us a line and let's discuss
|
Ready to build something exceptional? Drop us a line and let's discuss
|
||||||
how we can help you scale your digital presence.
|
how we can help you scale your digital presence.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { DataGrid } from "@repo/ui/procedural/data-grid";
|
import { DataGrid } from "@repo/ui/procedural/data-grid";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { Button } from "@repo/ui/components/button";
|
import { Button } from "@repo/ui/components/button";
|
||||||
|
import { SiteLogo } from "@/components/layout/site-logo";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Services | Such Software",
|
||||||
|
description: "Medusa v2 development, custom web applications, and cryptocurrency consulting. High-performance headless commerce, real-time platforms, and Web3 integration.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Services | Such Software",
|
||||||
|
description: "Medusa v2 development, custom web applications, and cryptocurrency consulting.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function ServicesPage() {
|
export default function ServicesPage() {
|
||||||
return (
|
return (
|
||||||
@@ -11,10 +23,10 @@ export default function ServicesPage() {
|
|||||||
{/* Option B: Data Grid Background for Technical Feel */}
|
{/* Option B: Data Grid Background for Technical Feel */}
|
||||||
<DataGrid />
|
<DataGrid />
|
||||||
|
|
||||||
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-sm sticky top-0">
|
<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-6">
|
<div className="flex items-center gap-8">
|
||||||
<Link href="/" className="text-2xl font-bold tracking-tighter">Such Software</Link>
|
<SiteLogo />
|
||||||
<nav className="hidden md:flex gap-4 text-sm font-medium">
|
<nav className="hidden md:flex gap-6 text-sm font-medium">
|
||||||
<Link href="/services" className="text-primary transition-colors">Services</Link>
|
<Link href="/services" className="text-primary transition-colors">Services</Link>
|
||||||
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -25,7 +37,7 @@ export default function ServicesPage() {
|
|||||||
<div className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
|
<div className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
|
||||||
{/* Context Header */}
|
{/* Context Header */}
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-emerald-500/50 pl-6">
|
<div className="text-left mb-8 px-2 border-l-2 border-emerald-500/50 section-container">
|
||||||
<h1 className="text-4xl md:text-6xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-blue-500">
|
<h1 className="text-4xl md:text-6xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-blue-500">
|
||||||
Specialized Solutions
|
Specialized Solutions
|
||||||
</h1>
|
</h1>
|
||||||
@@ -39,7 +51,7 @@ export default function ServicesPage() {
|
|||||||
|
|
||||||
{/* Medusa / Webshops Section */}
|
{/* Medusa / Webshops Section */}
|
||||||
<div className="mb-24">
|
<div className="mb-24">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-blue-500/50 pl-6">
|
<div className="text-left mb-8 px-2 border-l-2 border-blue-500/50 section-container mb-10">
|
||||||
<h2 className="text-3xl font-bold mb-2">Medusa v2 Development</h2>
|
<h2 className="text-3xl font-bold mb-2">Medusa v2 Development</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<p className="text-muted-foreground text-lg max-w-3xl">
|
||||||
We build proprietary, headless commerce engines that scale with your business logic.
|
We build proprietary, headless commerce engines that scale with your business logic.
|
||||||
@@ -48,23 +60,23 @@ export default function ServicesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-indigo-500/10 hover:border-indigo-500/30 transition-all">
|
<Card className="glass-card hover:border-indigo-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Unlimited Data Modeling</h3>
|
<h3 className="text-xl font-bold mb-3">Unlimited Data Modeling</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Natively model complex entities like "DNA Sequences" or "Royalty Logic"
|
Natively model complex entities like "DNA Sequences" or "Royalty Logic"
|
||||||
directly in the database without hacky metadata.
|
directly in the database without hacky metadata.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-indigo-500/10 hover:border-indigo-500/30 transition-all">
|
<Card className="glass-card hover:border-indigo-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Zero Capital Upfront</h3>
|
<h3 className="text-xl font-bold mb-3">Zero Capital Upfront</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Our "Toast" style partnership means we win when you win. Low friction onboarding
|
Our "Toast" style partnership means we win when you win. Low friction onboarding
|
||||||
with performance-based incentives.
|
with performance-based incentives.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-indigo-500/10 hover:border-indigo-500/30 transition-all">
|
<Card className="glass-card hover:border-indigo-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Headless SEO</h3>
|
<h3 className="text-xl font-bold mb-3">Headless SEO</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Custom Next.js frontends engineered for perfect lighthouse scores and
|
Custom Next.js frontends engineered for perfect lighthouse scores and
|
||||||
superior organic search conversion.
|
superior organic search conversion.
|
||||||
</p>
|
</p>
|
||||||
@@ -74,24 +86,24 @@ export default function ServicesPage() {
|
|||||||
|
|
||||||
{/* Custom Apps Section */}
|
{/* Custom Apps Section */}
|
||||||
<div className="mb-24">
|
<div className="mb-24">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-purple-500/50 pl-6">
|
<div className="text-left mb-8 px-2 border-l-2 border-purple-500/50 section-container mb-10">
|
||||||
<h2 className="text-3xl font-bold mb-2">Custom Apps & Interactive Websites</h2>
|
<h2 className="text-3xl font-bold mb-2">Custom Apps & Interactive Websites</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-2xl">
|
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
|
||||||
Whether it's a real-time collaborative tool or a high-end brand experience,
|
Whether it's a real-time collaborative tool or a high-end brand experience,
|
||||||
we build performant web applications that delight users.
|
we build performant web applications that delight users.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-purple-500/10 hover:border-purple-500/30 transition-all">
|
<Card className="glass-card hover:border-purple-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Real-time Platforms</h3>
|
<h3 className="text-xl font-bold mb-3">Real-time Platforms</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Socket.io and WebRTC integration for collaborative tools, dashboards,
|
Socket.io and WebRTC integration for collaborative tools, dashboards,
|
||||||
and live data visualization.
|
and live data visualization.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-purple-500/10 hover:border-purple-500/30 transition-all">
|
<Card className="glass-card hover:border-purple-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Brand Motion</h3>
|
<h3 className="text-xl font-bold mb-3">Brand Motion</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Framermotion and Three.js powered interfaces that tell your brand's story
|
Framermotion and Three.js powered interfaces that tell your brand's story
|
||||||
through motion and depth.
|
through motion and depth.
|
||||||
</p>
|
</p>
|
||||||
@@ -101,24 +113,24 @@ export default function ServicesPage() {
|
|||||||
|
|
||||||
{/* Crypto Section */}
|
{/* Crypto Section */}
|
||||||
<div className="mb-24">
|
<div className="mb-24">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-cyan-500/50 pl-6">
|
<div className="text-left mb-8 px-2 border-l-2 border-cyan-500/50 section-container mb-10">
|
||||||
<h2 className="text-3xl font-bold mb-2">Cryptocurrency & Payment Consulting</h2>
|
<h2 className="text-3xl font-bold mb-2">Cryptocurrency & Payment Consulting</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-2xl">
|
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
|
||||||
Navigate the complex world of global finance. We bridge the gap between
|
Navigate the complex world of global finance. We bridge the gap between
|
||||||
traditional banking and decentralized protocols.
|
traditional banking and decentralized protocols.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-cyan-500/10 hover:border-cyan-500/30 transition-all">
|
<Card className="glass-card hover:border-cyan-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Payment Orchestration</h3>
|
<h3 className="text-xl font-bold mb-3">Payment Orchestration</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Multi-PSP routing, automated reconciliation, and fraud prevention for
|
Multi-PSP routing, automated reconciliation, and fraud prevention for
|
||||||
high-volume merchants.
|
high-volume merchants.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-6 bg-card/80 backdrop-blur-sm border-cyan-500/10 hover:border-cyan-500/30 transition-all">
|
<Card className="glass-card hover:border-cyan-500/30">
|
||||||
<h3 className="text-xl font-bold mb-2">Web3 Integration</h3>
|
<h3 className="text-xl font-bold mb-3">Web3 Integration</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Non-custodial wallet auth, on/off ramp solutions, and smart contract
|
Non-custodial wallet auth, on/off ramp solutions, and smart contract
|
||||||
interactions for modern dApps.
|
interactions for modern dApps.
|
||||||
</p>
|
</p>
|
||||||
@@ -126,16 +138,16 @@ export default function ServicesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-20 p-12 rounded-2xl bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 text-white text-center shadow-2xl relative overflow-hidden group">
|
<div className="mt-20 p-16 rounded-[2.5rem] bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 text-white text-center shadow-2xl relative overflow-hidden group border-2 border-white/5">
|
||||||
<div className="absolute inset-0 bg-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute inset-0 bg-blue-500/10 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
<h2 className="text-3xl font-bold mb-4 relative z-10">Start building the future</h2>
|
<h2 className="text-4xl font-bold mb-6 relative z-10 tracking-tight">Start building the future</h2>
|
||||||
<p className="mb-8 text-slate-300 max-w-xl mx-auto relative z-10">
|
<p className="mb-10 text-slate-300 text-lg max-w-2xl mx-auto relative z-10 leading-relaxed font-medium">
|
||||||
We are currently accepting a limited number of partners for performance-based development.
|
We are currently accepting a limited number of partners for performance-based development.
|
||||||
Let's see if we're a fit.
|
Let's see if we're a fit.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/#contact" className="relative z-10">
|
<Link href="/#contact" className="relative z-10">
|
||||||
<Button size="lg" variant="secondary" className="rounded-full px-12 py-6 text-lg hover:scale-105 transition-transform">
|
<Button size="lg" variant="secondary" className="rounded-2xl px-12 py-8 text-xl font-bold hover:scale-105 transition-transform bg-white text-slate-900 hover:bg-slate-100">
|
||||||
Request a Demo
|
Request a Consultation
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,48 +1,82 @@
|
|||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export const FounderSignature = () => {
|
export const FounderSignature = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-4xl mx-auto py-20 px-4">
|
<div className="grid md:grid-cols-[1fr,440px] gap-12 lg:gap-20 items-start">
|
||||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
{/* Left Column: Bio */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-8 text-left section-container">
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Accessibility as Architecture</h2>
|
<div className="flex items-center gap-6 relative z-10">
|
||||||
<div className="prose dark:prose-invert">
|
<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">
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
<Image
|
||||||
"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.
|
src="/images/logo_w_circle_trans.jpg"
|
||||||
</p>
|
alt="Such Software Icon"
|
||||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
fill
|
||||||
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.
|
className="object-cover scale-[1.02]"
|
||||||
</p>
|
/>
|
||||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
</div>
|
||||||
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."
|
<div>
|
||||||
</p>
|
<p className="font-mono text-xs uppercase tracking-[0.4em] text-primary/80 mb-1">Founder & Principal Engineer</p>
|
||||||
</div>
|
<h2 className="text-3xl font-bold tracking-tight">John Winter Murphy</h2>
|
||||||
<div className="pt-4 border-t border-border">
|
</div>
|
||||||
<p className="font-mono text-sm uppercase tracking-widest text-primary">Founder & Principle Engineer</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="relative p-8 bg-muted/30 border-dashed">
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
<div className="absolute -top-4 -right-4 w-24 h-24 bg-blue-500/10 rounded-full blur-2xl" />
|
<p className="text-muted-foreground leading-relaxed italic border-l-4 border-primary/20 pl-6 text-lg">
|
||||||
<div className="absolute -bottom-4 -left-4 w-32 h-32 bg-purple-500/10 rounded-full blur-2xl" />
|
"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.
|
||||||
|
</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.
|
||||||
|
</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."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 className="tex-lg font-semibold mb-4">Core Competencies</h3>
|
{/* Right Column: Cards 01, 02, 03 */}
|
||||||
<ul className="space-y-3 font-mono text-sm">
|
<div className="space-y-6 lg:-mt-4">
|
||||||
<li className="flex items-center gap-2">
|
<Card className="glass-card hover:border-purple-500/30">
|
||||||
<span className="text-green-500">✓</span> WCAG 2.2 AA Compliance
|
<h4 className="text-base font-bold mb-3 flex items-center gap-2">
|
||||||
</li>
|
<span className="w-6 h-6 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500 text-[10px]">01</span>
|
||||||
<li className="flex items-center gap-2">
|
A11y First Architecture
|
||||||
<span className="text-green-500">✓</span> Section 508 & ADA
|
</h4>
|
||||||
</li>
|
<p className="text-muted-foreground text-sm leading-relaxed font-medium">
|
||||||
<li className="flex items-center gap-2">
|
Semantic HTML, ARIA patterns, and keyboard navigation aren't afterthoughts—they are the foundation of our build process.
|
||||||
<span className="text-green-500">✓</span> Semantic HTML5 Architecture
|
</p>
|
||||||
</li>
|
</Card>
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="text-green-500">✓</span> Playwright A11y Automation
|
<Card className="glass-card hover:border-blue-500/30">
|
||||||
</li>
|
<h4 className="text-base font-bold mb-3 flex items-center gap-2">
|
||||||
|
<span className="w-6 h-6 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500 text-[10px]">02</span>
|
||||||
|
Edge Performance
|
||||||
|
</h4>
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed font-medium">
|
||||||
|
By leveraging edge computing and static generation, we ensure your site is usable even on 3G connections and older hardware.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="glass-card hover:border-emerald-500/30 border-dashed">
|
||||||
|
<h4 className="text-base font-bold mb-4 flex items-center gap-2">
|
||||||
|
<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">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="text-emerald-500 text-sm">✓</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
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="text-emerald-500 text-sm">✓</span> Semantic HTML5 Architecture
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="text-emerald-500 text-sm">✓</span> Playwright A11y Automation
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useFormState, useFormStatus } from 'react-dom'; // Note: check react-dom version for useFormState support or import from hook
|
import { useFormState, useFormStatus } from 'react-dom';
|
||||||
// Next.js 14 uses react-dom for these
|
|
||||||
import { submitContactForm } from '@/actions/submit-contact';
|
import { submitContactForm } from '@/actions/submit-contact';
|
||||||
import { TurnstileWidget } from './turnstile-widget';
|
import { TurnstileWidget } from './turnstile-widget';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@repo/ui/components/button';
|
import { Button } from '@repo/ui/components/button';
|
||||||
|
|
||||||
function SubmitButton() {
|
function SubmitButton() {
|
||||||
const { pending } = useFormStatus();
|
const { pending } = useFormStatus();
|
||||||
return (
|
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"
|
||||||
|
>
|
||||||
{pending ? "Sending..." : "Send Message"}
|
{pending ? "Sending..." : "Send Message"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -20,38 +24,84 @@ export function ContactForm() {
|
|||||||
const [token, setToken] = useState<string>('');
|
const [token, setToken] = useState<string>('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={formAction} className="space-y-4 max-w-md mx-auto p-4 border rounded-lg bg-card text-card-foreground">
|
<form
|
||||||
<h3 className="text-xl font-bold">Contact Us</h3>
|
action={formAction}
|
||||||
<div>
|
className="space-y-8 w-full max-w-6xl mx-auto section-container"
|
||||||
<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" />
|
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||||
{state?.errors?.name && <p className="text-red-500 text-xs">{state.errors.name}</p>}
|
<div className="absolute bottom-0 left-0 w-64 h-64 bg-blue-500/5 rounded-full blur-3xl -ml-32 -mb-32" />
|
||||||
|
|
||||||
|
<div className="text-left mb-10 relative z-10">
|
||||||
|
<h3 className="text-3xl font-bold tracking-tight">Project Inquiry</h3>
|
||||||
|
<p className="text-muted-foreground text-lg mt-3 max-w-2xl text-balance">
|
||||||
|
Tell us about your technical challenges. From Medusa v2 migrations to real-time custom engines, we're ready to architect your solution.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
{state?.errors?.name && <p className="text-red-500 text-sm px-1">{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"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
{state?.errors?.email && <p className="text-red-500 text-sm px-1">{state.errors.email}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-3 relative z-10">
|
||||||
<label htmlFor="email" className="block text-sm font-medium">Email</label>
|
<label htmlFor="message" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Project Details</label>
|
||||||
<input name="email" id="email" type="email" required className="w-full mt-1 p-2 border rounded bg-background" />
|
<textarea
|
||||||
{state?.errors?.email && <p className="text-red-500 text-xs">{state.errors.email}</p>}
|
name="message"
|
||||||
</div>
|
id="message"
|
||||||
|
placeholder="What are you building? Mention any specific constraints, timelines, or technologies..."
|
||||||
<div>
|
required
|
||||||
<label htmlFor="message" className="block text-sm font-medium">Message</label>
|
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"
|
||||||
<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>}
|
{state?.errors?.message && <p className="text-red-500 text-sm px-1">{state.errors.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="token" value={token} />
|
<input type="hidden" name="token" value={token} />
|
||||||
|
|
||||||
<div className="my-4">
|
<div className="flex flex-col md:flex-row items-center justify-between gap-8 pt-4 relative z-10 border-t border-border/50">
|
||||||
<TurnstileWidget onVerify={setToken} />
|
<div className="scale-100 flex items-center gap-3">
|
||||||
|
<TurnstileWidget onVerify={setToken} />
|
||||||
|
<p className="text-[10px] text-muted-foreground max-w-[120px] leading-tight">
|
||||||
|
Protected by Cloudflare Turnstile to ensure human delivery.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-auto">
|
||||||
|
<SubmitButton />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubmitButton />
|
|
||||||
|
|
||||||
{state?.message && (
|
{state?.message && (
|
||||||
<p className={`text-sm ${state.success ? 'text-green-500' : 'text-red-500'}`}>
|
<div className={`mt-8 p-6 rounded-2xl text-base border-l-4 relative z-10 ${
|
||||||
{state.message}
|
state.success
|
||||||
</p>
|
? "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>
|
||||||
|
<p className="font-medium">{state.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,33 +2,33 @@ import { Card } from "@repo/ui/components/card";
|
|||||||
|
|
||||||
export function ServicesPreview() {
|
export function ServicesPreview() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto px-4 mt-8 mb-16">
|
<div className="w-full max-w-6xl mx-auto px-4 mt-8 mb-16 relative z-10">
|
||||||
<div className="text-left mb-8 px-2 border-l-2 border-emerald-500/50 pl-6">
|
<div className="text-left mb-8 px-2 border-l-2 border-emerald-500/50 section-container">
|
||||||
<h3 className="text-2xl font-bold mb-2">High-Performance Webshops</h3>
|
<h3 className="text-3xl font-bold mb-3 tracking-tight">High-Performance Webshops</h3>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
We build commerce infrastructure that scales with you. From complex supply chains
|
We build commerce infrastructure that scales with you. From complex supply chains
|
||||||
to global payments, we handle the technical heavy lifting so you can focus on your brand.
|
to global payments, we handle the technical heavy lifting so you can focus on your brand.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
<Card className="p-6 bg-card/50 backdrop-blur-sm border-indigo-500/20 hover:border-indigo-500/40 transition-colors text-left">
|
<Card className="glass-card hover:border-indigo-500/40 text-left">
|
||||||
<h3 className="text-xl font-bold mb-2">Native Crypto & Payments</h3>
|
<h3 className="text-xl font-bold mb-3">Native Crypto & Payments</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
Deep expertise in multi-currency checkout, fiat-to-crypto onboarding, and
|
Deep expertise in multi-currency checkout, fiat-to-crypto onboarding, and
|
||||||
DeFi integration. We build payments that work everywhere, for everyone.
|
DeFi integration. We build payments that work everywhere, for everyone.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-6 bg-card/50 backdrop-blur-sm border-indigo-500/20 hover:border-indigo-500/40 transition-colors text-left">
|
<Card className="glass-card hover:border-indigo-500/40 text-left">
|
||||||
<h3 className="text-xl font-bold mb-2">Zero Capital Upfront</h3>
|
<h3 className="text-xl font-bold mb-3">Zero Capital Upfront</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
We operate on a "Toast" style partnership model. No massive setup fees or
|
We operate on a "Toast" style partnership model. No massive setup fees or
|
||||||
stiff monthly subscriptions—we only win when you're making sales.
|
stiff monthly subscriptions—we only win when you're making sales.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-6 bg-card/50 backdrop-blur-sm border-indigo-500/20 hover:border-indigo-500/40 transition-colors text-left">
|
<Card className="glass-card hover:border-indigo-500/40 text-left">
|
||||||
<h3 className="text-xl font-bold mb-2">Headless Performance</h3>
|
<h3 className="text-xl font-bold mb-3">Headless Performance</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-base text-muted-foreground leading-relaxed">
|
||||||
We couple the robust Medusa backend with a Next.js frontend, ensuring your shop
|
We couple the robust Medusa backend with a Next.js frontend, ensuring your shop
|
||||||
scores 100/100 on Core Web Vitals and SEO.
|
scores 100/100 on Core Web Vitals and SEO.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { SiteLogo } from "./site-logo";
|
||||||
import { FaXTwitter, FaTelegram, FaReddit, FaPhone } from "react-icons/fa6";
|
import { FaXTwitter, FaTelegram, FaReddit, FaPhone } from "react-icons/fa6";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
@@ -9,7 +10,7 @@ export function Footer() {
|
|||||||
<div className="container px-4 md:px-6 mx-auto">
|
<div className="container px-4 md:px-6 mx-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-bold">Such Software</h3>
|
<SiteLogo />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Full-service software provider in the heart of Mushroom Country.
|
Full-service software provider in the heart of Mushroom Country.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
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">
|
||||||
|
{/* Dark mode logo (white text) */}
|
||||||
|
<Image
|
||||||
|
src="/images/logo_dark_w_name_trans.png"
|
||||||
|
alt="Such Software"
|
||||||
|
fill
|
||||||
|
className="object-contain hidden dark:block"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
{/* Light mode logo (black text) */}
|
||||||
|
<Image
|
||||||
|
src="/images/logo_light_w_name_trans.png"
|
||||||
|
alt="Such Software"
|
||||||
|
fill
|
||||||
|
className="object-contain block dark:hidden"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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/70 backdrop-blur-sm">
|
|
||||||
<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>
|
|
||||||
<Image
|
|
||||||
src="/images/r-cryptocurrency.png"
|
|
||||||
alt="r/CryptoCurrency Governance Dashboard"
|
|
||||||
fill
|
|
||||||
className="object-cover opacity-60 transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Secondary: Occupy Wallets */}
|
|
||||||
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100/70 backdrop-blur-sm">
|
|
||||||
<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 drop-shadow-sm">Occupy Wallets</h3>
|
|
||||||
<p className="text-zinc-800 font-medium drop-shadow-sm">High-fidelity gallery for fiscal graffiti.</p>
|
|
||||||
</div>
|
|
||||||
<Image
|
|
||||||
src="/images/occupy-wallets.png"
|
|
||||||
alt="Occupy Wallets Gallery"
|
|
||||||
fill
|
|
||||||
className="object-cover opacity-40 transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
|
||||||
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/70 dark:bg-orange-950/70 border-orange-500/20 backdrop-blur-sm">
|
|
||||||
<div className="absolute inset-0 z-10 p-6 bg-gradient-to-br from-background/90 to-transparent">
|
|
||||||
<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 text-foreground">
|
|
||||||
<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>
|
|
||||||
<Image
|
|
||||||
src="/images/privacy-labs.png"
|
|
||||||
alt="Privacy Labs Projects"
|
|
||||||
fill
|
|
||||||
className="object-cover opacity-30 transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Quaternary: MoonPlace */}
|
|
||||||
<Card className="md:col-span-1 relative group overflow-hidden bg-indigo-50/70 dark:bg-indigo-950/70 border-indigo-500/20 backdrop-blur-sm">
|
|
||||||
<div className="absolute inset-0 z-10 p-6 bg-gradient-to-br from-background/90 to-transparent">
|
|
||||||
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
|
|
||||||
<p className="text-sm mt-2 text-foreground">Collaborative pixel canvas engine.</p>
|
|
||||||
</div>
|
|
||||||
<Image
|
|
||||||
src="/images/moonplace.png"
|
|
||||||
alt="MoonPlace Pixel Canvas"
|
|
||||||
fill
|
|
||||||
className="object-cover opacity-30 transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -5,29 +5,31 @@ export const PortfolioGrid = () => {
|
|||||||
return (
|
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">
|
<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 */}
|
{/* Hero Project: r/CryptoCurrency */}
|
||||||
<Card className="md:col-span-2 md:row-span-2 relative group overflow-hidden border-0 bg-slate-900/70 backdrop-blur-sm min-h-[300px]">
|
<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/90 via-black/20 to-transparent p-8 flex flex-col justify-end pointer-events-none">
|
<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-3xl font-bold text-white shadow-black drop-shadow-xl">r/CryptoCurrency</h3>
|
<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-sm mt-2 font-medium">Governance dashboard for 10M+ members. Real-time DAO metrics.</p>
|
<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>
|
||||||
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-slate-800/50">
|
<div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30">
|
||||||
<Image
|
<Image
|
||||||
src="/images/r-cryptocurrency.png"
|
src="/images/r-cryptocurrency.png"
|
||||||
alt="r/CryptoCurrency"
|
alt="r/CryptoCurrency"
|
||||||
fill
|
fill
|
||||||
className="object-contain p-4 opacity-70 transition-transform duration-1000 group-hover:scale-105"
|
className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105"
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Secondary: Occupy Wallets */}
|
{/* Secondary: Occupy Wallets */}
|
||||||
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100/70 backdrop-blur-sm border-0 min-h-[300px]">
|
<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">
|
<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 drop-shadow-sm bg-white/60 backdrop-blur-md rounded-xl px-6 py-2">Occupy Wallets</h3>
|
<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 font-bold drop-shadow-sm bg-white/60 backdrop-blur-md rounded-xl px-6 py-2 mt-4">High-fidelity gallery for fiscal graffiti.</p>
|
<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>
|
||||||
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50">
|
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50">
|
||||||
<Image
|
<Image
|
||||||
src="/images/occupy-wallets.png"
|
src="/images/occupy-wallets.png"
|
||||||
alt="Occupy Wallets"
|
alt="Occupy Wallets"
|
||||||
@@ -39,7 +41,7 @@ export const PortfolioGrid = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
||||||
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/70 dark:bg-orange-950/70 border-orange-500/20 backdrop-blur-sm min-h-[300px]">
|
<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">
|
<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>
|
<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">
|
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
|
||||||
@@ -59,7 +61,7 @@ export const PortfolioGrid = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Quaternary: MoonPlace */}
|
{/* Quaternary: MoonPlace */}
|
||||||
<Card className="md:col-span-1 relative group overflow-hidden bg-indigo-50/70 dark:bg-indigo-950/70 border-indigo-500/20 backdrop-blur-sm min-h-[300px]">
|
<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">
|
<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>
|
<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>
|
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border) / <alpha-value>)",
|
||||||
input: "hsl(var(--input))",
|
input: "hsl(var(--input))",
|
||||||
ring: "hsl(var(--ring))",
|
ring: "hsl(var(--ring))",
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background) / <alpha-value>)",
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: "hsl(var(--foreground))",
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
@@ -48,7 +48,7 @@ const config: Config = {
|
|||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: "hsl(var(--card-foreground))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion, useAnimation } from "framer-motion";
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
|
||||||
import { cn } from "../lib/utils";
|
|
||||||
import { Monitor } from "lucide-react";
|
|
||||||
|
|
||||||
interface NebulaProps {
|
|
||||||
className?: string;
|
|
||||||
density?: number;
|
|
||||||
themeMode?: 'light' | 'dark'; // customized prop
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StreamLineProps {
|
|
||||||
d: string;
|
|
||||||
themeMode: 'light' | 'dark';
|
|
||||||
onHit: () => void;
|
|
||||||
width: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
|
|
||||||
const controls = useAnimation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
const animate = async () => {
|
|
||||||
// Random initial delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 4000));
|
|
||||||
|
|
||||||
while (isMounted) {
|
|
||||||
const duration = 2 + Math.random() * 4;
|
|
||||||
|
|
||||||
controls.set({ pathLength: 0, opacity: 0 });
|
|
||||||
|
|
||||||
await controls.start({
|
|
||||||
pathLength: [0, 1],
|
|
||||||
opacity: [0, 1, 1, 0],
|
|
||||||
transition: {
|
|
||||||
duration,
|
|
||||||
ease: "easeInOut",
|
|
||||||
times: [0, 0.1, 0.9, 1]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isMounted) onHit();
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
animate();
|
|
||||||
return () => { isMounted = false; };
|
|
||||||
}, [controls, onHit]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.path
|
|
||||||
d={d}
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={width}
|
|
||||||
fill="none"
|
|
||||||
animate={controls}
|
|
||||||
className={cn(
|
|
||||||
"transition-colors duration-500",
|
|
||||||
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: NebulaProps) => {
|
|
||||||
const [paths, setPaths] = useState<string[]>([]);
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const monitorControls = useAnimation();
|
|
||||||
|
|
||||||
// Configuration for convergence point (percent of container)
|
|
||||||
const targetX = 25; // moved right
|
|
||||||
const targetY = 20; // moved up
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
const newPaths = Array.from({ length: density }).map(() => {
|
|
||||||
// Determine random edge start point
|
|
||||||
const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
|
|
||||||
let startX = 50, startY = 50;
|
|
||||||
|
|
||||||
// Push start point well outside viewbox to ensure full coverage
|
|
||||||
switch(edge) {
|
|
||||||
case 0: startX = Math.random() * 100; startY = -20; break; // Top
|
|
||||||
case 1: startX = 120; startY = Math.random() * 100; break; // Right
|
|
||||||
case 2: startX = Math.random() * 100; startY = 120; break; // Bottom
|
|
||||||
case 3: startX = -20; startY = Math.random() * 100; break; // Left
|
|
||||||
}
|
|
||||||
|
|
||||||
// End at target point with small variance
|
|
||||||
const endX = targetX + (Math.random() - 0.5) * 5;
|
|
||||||
const endY = targetY + (Math.random() - 0.5) * 5;
|
|
||||||
|
|
||||||
// Control point for curve
|
|
||||||
const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50;
|
|
||||||
const cpY = (startY + endY) / 2 + (Math.random() - 0.5) * 50;
|
|
||||||
|
|
||||||
return `M ${startX} ${startY} Q ${cpX} ${cpY} ${endX} ${endY}`;
|
|
||||||
});
|
|
||||||
setPaths(newPaths);
|
|
||||||
}, [density]); // Removed targetX/Y dep as constant
|
|
||||||
|
|
||||||
const handleHit = useCallback(() => {
|
|
||||||
monitorControls.start({
|
|
||||||
scale: [1, 1.15, 1],
|
|
||||||
filter: ["brightness(1)", "brightness(1.5)", "brightness(1)"],
|
|
||||||
transition: { duration: 0.3 }
|
|
||||||
});
|
|
||||||
}, [monitorControls]);
|
|
||||||
|
|
||||||
if (!mounted) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
|
|
||||||
|
|
||||||
{/* "Computer" Icon positioned via inline styles to match physics target exactly */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute -translate-x-1/2 -translate-y-1/2 z-10 opacity-30 dark:opacity-50"
|
|
||||||
style={{ left: `${targetX}%`, top: `${targetY}%` }}
|
|
||||||
animate={monitorControls}
|
|
||||||
>
|
|
||||||
<Monitor
|
|
||||||
size={64}
|
|
||||||
className={cn(
|
|
||||||
"transition-colors duration-1000",
|
|
||||||
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
className="w-full h-full opacity-60 dark:opacity-80 absolute inset-0"
|
|
||||||
>
|
|
||||||
{paths.map((d, i) => (
|
|
||||||
<StreamLine
|
|
||||||
key={i}
|
|
||||||
d={d}
|
|
||||||
themeMode={themeMode}
|
|
||||||
onHit={handleHit}
|
|
||||||
width={themeMode === 'light' ? 0.1 : 0.2}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||