mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +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:
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Turborepo
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
# Environment configurations
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
*.env
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.cache/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# AI / Assistant
|
||||||
|
.claude/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["@repo/config/eslint-preset.js"],
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
transpilePackages: ["@repo/ui"],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@marsidev/react-turnstile": "^1.0.2",
|
||||||
|
"@repo/config": "workspace:*",
|
||||||
|
"@repo/ui": "workspace:*",
|
||||||
|
"framer-motion": "^11.0.0",
|
||||||
|
"lucide-react": "^0.350.0",
|
||||||
|
"next": "14.1.0",
|
||||||
|
"next-themes": "^0.2.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"autoprefixer": "^10.0.1",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.1.0",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.3.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { verifyTurnstile } from '@/lib/turnstile'; // Helper to call Cloudflare API
|
||||||
|
|
||||||
|
const ContactSchema = z.object({
|
||||||
|
name: z.string().min(2, "Name is too short"),
|
||||||
|
email: z.string().email("Invalid email address"),
|
||||||
|
message: z.string().min(10, "Please tell us more about your project"),
|
||||||
|
token: z.string().min(1, "Security check failed")
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function submitContactForm(prevState: any, formData: FormData) {
|
||||||
|
const rawData = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
// 1. Zod Validation
|
||||||
|
const validated = ContactSchema.safeParse(rawData);
|
||||||
|
if (!validated.success) {
|
||||||
|
return { success: false, errors: validated.error.flatten().fieldErrors };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Turnstile Verification
|
||||||
|
const isHuman = await verifyTurnstile(validated.data.token);
|
||||||
|
if (!isHuman) {
|
||||||
|
return { success: false, message: "Security check failed. Please refresh." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Send to Backend (or Email)
|
||||||
|
try {
|
||||||
|
// TODO: Connect to email service or CMS
|
||||||
|
console.log("Form submitted:", validated.data);
|
||||||
|
|
||||||
|
// Simulating success for now
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
return { success: true, message: "Request received. We will be in touch shortly." };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return { success: false, message: "System error. Please contact us via email." };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Such Software LLC",
|
||||||
|
description: "Next-Generation Monorepo Platform",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body className={inter.className}>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
|
import { PortfolioGrid } from "@/components/portfolio/bento-grid";
|
||||||
|
import { ContactForm } from "@/components/contact/contact-form";
|
||||||
|
import { FounderSignature } from "@/components/about/founder-signature";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@repo/ui/components/button";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="relative min-h-screen flex flex-col items-center justify-between overflow-hidden bg-background text-foreground transition-colors duration-500">
|
||||||
|
|
||||||
|
<DynamicBackground />
|
||||||
|
|
||||||
|
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tighter">Such Software</h1>
|
||||||
|
<nav className="hidden md:flex gap-4 text-sm font-medium">
|
||||||
|
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
|
||||||
|
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
|
||||||
|
<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">
|
||||||
|
Digital Craftsmanship
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-2xl text-xl text-muted-foreground mb-8">
|
||||||
|
Next-generation web applications engineered for scale, compliance, and performance.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mb-20">
|
||||||
|
<Link href="/services">
|
||||||
|
<Button size="lg" className="rounded-full">
|
||||||
|
Explore Services
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="#contact">
|
||||||
|
<Button variant="outline" size="lg" className="rounded-full">
|
||||||
|
Start a Project
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 w-full">
|
||||||
|
<h3 className="text-3xl font-bold text-left w-full mt-10 mb-6 px-4">Selected Works</h3>
|
||||||
|
<PortfolioGrid />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FounderSignature />
|
||||||
|
|
||||||
|
<div id="contact" className="w-full mt-20 mb-10 flex flex-col items-center">
|
||||||
|
<h3 className="text-3xl font-bold mb-8">Start a Project</h3>
|
||||||
|
<ContactForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { DataGrid } from "@repo/ui/procedural/data-grid";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
|
import { Button } from "@repo/ui/components/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Card } from "@repo/ui/components/card";
|
||||||
|
|
||||||
|
export default function ServicesPage() {
|
||||||
|
return (
|
||||||
|
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground overflow-hidden">
|
||||||
|
|
||||||
|
{/* Option B: Data Grid Background for Technical Feel */}
|
||||||
|
<DataGrid />
|
||||||
|
|
||||||
|
<header 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">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<Link href="/" className="text-2xl font-bold tracking-tighter">Such Software</Link>
|
||||||
|
<nav className="hidden md:flex gap-4 text-sm font-medium">
|
||||||
|
<Link href="/services" className="text-primary transition-colors">Services</Link>
|
||||||
|
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
|
||||||
|
<div className="mb-16 text-center">
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-cyan-500">
|
||||||
|
Medusa v2 Development
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
We build proprietary, headless commerce engines that scale with your business logic.
|
||||||
|
Stop fighting with Shopify's limitations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<Card className="p-6 bg-card/80 backdrop-blur-sm border-indigo-500/20">
|
||||||
|
<h3 className="text-xl font-bold mb-2">Unlimited Data Modeling</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Need a product with "DNA Sequence", "Artist Bio", and "Royalty Logic"?
|
||||||
|
We model that natively in the database, not via hacky meta-fields.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6 bg-card/80 backdrop-blur-sm border-indigo-500/20">
|
||||||
|
<h3 className="text-xl font-bold mb-2">Workflow Orchestration</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Automate complex business logic—like splitting an order between a print-on-demand provider
|
||||||
|
and a local warehouse automatically using Medusa Workflows.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6 bg-card/80 backdrop-blur-sm border-indigo-500/20">
|
||||||
|
<h3 className="text-xl font-bold mb-2">Headless Performance</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
We couple the robust Medusa backend with a Next.js 16 frontend, ensuring your shop
|
||||||
|
scores 100/100 on Core Web Vitals and SEO.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-20 p-8 rounded-2xl bg-gradient-to-br from-slate-900 to-slate-800 text-white text-center">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Ready to upgrading your shop?</h2>
|
||||||
|
<p className="mb-8 text-slate-300">
|
||||||
|
We are currently accepting beta partners for our new Medusa v2 accelerator.
|
||||||
|
</p>
|
||||||
|
<Link href="/#contact">
|
||||||
|
<Button size="lg" variant="secondary" className="rounded-full">
|
||||||
|
Request a Demo
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export async function verifyTurnstile(token: string) {
|
||||||
|
const secretKey = process.env.TURNSTILE_SECRET_KEY;
|
||||||
|
if (!secretKey) {
|
||||||
|
console.warn("No TURNSTILE_SECRET_KEY set, skipping verification", token);
|
||||||
|
return true; // Dev mode bypass
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('secret', secretKey);
|
||||||
|
formData.append('response', token);
|
||||||
|
|
||||||
|
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data.success;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}"
|
||||||
|
],
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||||
|
"gradient-conic":
|
||||||
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "such-software-monorepo",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo run build",
|
||||||
|
"dev": "turbo run dev",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"clean": "turbo run clean && rm -rf node_modules",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"turbo": "latest",
|
||||||
|
"prettier": "^3.2.0",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["next/core-web-vitals"],
|
||||||
|
rules: {
|
||||||
|
// strict accessibility rules will go here later
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/config",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"files": [
|
||||||
|
"tsconfig.json",
|
||||||
|
"eslint-preset.js"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "14.1.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@repo/*": ["./packages/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
"./*": "./src/*.tsx"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/config": "workspace:*",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
|
"framer-motion": "^11.0.0",
|
||||||
|
"lucide-react": "^0.350.0",
|
||||||
|
"tailwind-merge": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
export { Card }
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
export const DataGrid = () => {
|
||||||
|
const gridLines = Array.from({ length: 10 }).map((_, i) => i * 10);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className="absolute inset-0 w-full h-full [mask-image:linear-gradient(to_bottom,transparent,black)] pointer-events-none z-0">
|
||||||
|
{gridLines.map((pos, i) => (
|
||||||
|
<motion.line
|
||||||
|
key={`h-${i}`}
|
||||||
|
x1="0" y1={`${pos}%`} x2="100%" y2={`${pos}%`}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1"
|
||||||
|
className="text-gray-200 dark:text-slate-800"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 2, delay: i * 0.1 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{/* Animated "Data Packets" travelling lines */}
|
||||||
|
<motion.rect
|
||||||
|
width="2" height="2" fill="currentColor"
|
||||||
|
className="text-blue-500 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
<motion.animateMotion
|
||||||
|
path="M 0 50 H 100"
|
||||||
|
dur="5s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</motion.rect>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
interface NebulaProps {
|
||||||
|
className?: string;
|
||||||
|
density?: number;
|
||||||
|
themeMode?: 'light' | 'dark'; // customized prop
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NebulaField = ({ className, density = 15, themeMode = 'dark' }: NebulaProps) => {
|
||||||
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Procedural generation: Create organic bezier curves
|
||||||
|
useEffect(() => {
|
||||||
|
const newPaths = Array.from({ length: density }).map(() => {
|
||||||
|
// Random control points
|
||||||
|
const startX = Math.random() * 100;
|
||||||
|
const startY = Math.random() * 100;
|
||||||
|
const cp1X = startX + (Math.random() - 0.5) * 50;
|
||||||
|
const cp1Y = startY + (Math.random() - 0.5) * 50;
|
||||||
|
const endX = Math.random() * 100;
|
||||||
|
const endY = Math.random() * 100;
|
||||||
|
|
||||||
|
return `M ${startX} ${startY} Q ${cp1X} ${cp1Y} ${endX} ${endY}`;
|
||||||
|
});
|
||||||
|
setPaths(newPaths);
|
||||||
|
}, [density]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
className="w-full h-full opacity-40 dark:opacity-60"
|
||||||
|
>
|
||||||
|
{paths.map((d, i) => (
|
||||||
|
<motion.path
|
||||||
|
key={i}
|
||||||
|
d={d}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={themeMode === 'light' ? 0.1 : 0.3}
|
||||||
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
pathLength: [0, 1, 0],
|
||||||
|
opacity: [0, 0.8, 0]
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 20 + Math.random() * 10, // Async timing
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
delay: Math.random() * 5
|
||||||
|
}}
|
||||||
|
className="text-slate-400 dark:text-emerald-400"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Generated
+4157
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": ["**/.env.*local"],
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": [".next/**", "!.next/cache/**", "dist/**", ".docusaurus/**"],
|
||||||
|
"env": ["NEXT_PUBLIC_TURNSTILE_SITE_KEY"]
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["^lint"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user