diff --git a/.gitignore b/.gitignore index 5ef6a52..f8b926f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,10 @@ yarn-error.log* # vercel .vercel +# superdesign +.superdesign + # typescript *.tsbuildinfo next-env.d.ts +.env*.local diff --git a/next.config.ts b/next.config.ts index e9ffa30..a9194aa 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "images.unsplash.com", + }, + ], + }, }; export default nextConfig; diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..00fe54f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,314 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; +@theme inline { + --color-c47-bg: #0c0a09; + --color-c47-accent: #FF6B50; + --color-c47-accent-dim: rgba(255, 107, 80, 0.15); + --color-c47-accent-glow: rgba(255, 107, 80, 0.4); + --color-c47-muted: #78716c; + --color-c47-border: #27272a; + --color-c47-card: #111111; + --color-c47-text: #ebebeb; + + --font-sans: var(--font-satoshi), var(--font-inter), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-jetbrains-mono), ui-monospace, monospace; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); +/* ── Base ────────────────────────────────────────── */ + +html { + scroll-behavior: smooth; +} + +body { + background: var(--color-c47-bg); + color: var(--color-c47-text); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +::selection { + background: var(--color-c47-accent); + color: #fff; +} + +/* ── Scroll Reveal ───────────────────────────────── */ + +.reveal { + opacity: 0; + transform: translateY(32px); + transition: opacity 0.7s cubic-bezier(0.4, 0, 0.2, 1), + transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); +} + +.reveal.visible { + opacity: 1; + transform: translateY(0); +} + +.reveal-delay-1 { transition-delay: 0.1s; } +.reveal-delay-2 { transition-delay: 0.2s; } +.reveal-delay-3 { transition-delay: 0.3s; } +.reveal-delay-4 { transition-delay: 0.4s; } + +/* ── Terminal Line Stagger ───────────────────────── */ + +.terminal-line { + opacity: 0; + transform: translateX(-8px); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.terminal-active .terminal-line:nth-child(1) { opacity: 1; transform: translateX(0); transition-delay: 0s; } +.terminal-active .terminal-line:nth-child(2) { opacity: 1; transform: translateX(0); transition-delay: 0.15s; } +.terminal-active .terminal-line:nth-child(3) { opacity: 1; transform: translateX(0); transition-delay: 0.3s; } + +/* ── Card Shimmer ────────────────────────────────── */ + +.card-shimmer { + position: relative; + overflow: hidden; +} + +.card-shimmer::after { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 50%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 107, 80, 0.04), transparent); + transition: none; + pointer-events: none; +} + +.card-shimmer:hover::after { + animation: shimmer 1.2s ease-out forwards; } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +@keyframes shimmer { + 0% { left: -50%; } + 100% { left: 150%; } +} + +/* ── CTA Glow Pulse ──────────────────────────────── */ + +@keyframes glow-pulse { + 0%, 100% { + box-shadow: 0 0 20px rgba(255, 107, 80, 0.2), + 0 0 40px rgba(255, 107, 80, 0.05); + } + 50% { + box-shadow: 0 0 28px rgba(255, 107, 80, 0.35), + 0 0 56px rgba(255, 107, 80, 0.1); } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +.cta-glow { + animation: glow-pulse 3s ease-in-out infinite; +} + +.cta-glow:hover { + animation: none; + box-shadow: 0 0 36px rgba(255, 107, 80, 0.5), + 0 0 72px rgba(255, 107, 80, 0.15); +} + +/* ── Step Number Bounce ──────────────────────────── */ + +@keyframes bounceStep { + 0% { transform: scale(1); } + 40% { transform: scale(1.08); } + 70% { transform: scale(0.98); } + 100% { transform: scale(1.05); } +} + +.step-number { + transition: color 0.35s ease, transform 0.35s ease; + display: inline-block; +} + +.group\/step:hover .step-number { + animation: bounceStep 0.35s ease-out forwards; + color: #FF6B50 !important; +} + +/* ── Float Animation ───────────────────────────── */ + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } +} + +.animate-float { + animation: float 4s ease-in-out infinite; +} + +.animate-float-delayed { + animation: float 4.5s ease-in-out 0.5s infinite; +} + +.animate-float-delayed-2 { + animation: float 5s ease-in-out 1s infinite; +} + +/* ── Newsletter Underline ────────────────────────── */ + +.newsletter-input { + background-image: linear-gradient(to right, #FF6B50, #FF9A85); + background-size: 0% 2px; + background-position: left bottom; + background-repeat: no-repeat; + transition: background-size 0.4s cubic-bezier(0.4, 0, 0.2, 1), + border-color 0.4s ease; +} + +.newsletter-input:focus { + background-size: 100% 2px; + border-color: transparent !important; +} + +/* ── Gradient Border Glow ────────────────────────── */ + +.gradient-border-glow { + position: relative; +} + +.gradient-border-glow::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: linear-gradient(135deg, rgba(255, 107, 80, 0.3), transparent 50%, rgba(255, 107, 80, 0.1)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0; + transition: opacity 0.5s ease; +} + +.gradient-border-glow:hover::before { + opacity: 1; +} + +/* ── Gradient text helpers ───────────────────────── */ + +.gradient-text { + background: linear-gradient(135deg, #FF6B50 0%, #FF9A85 40%, #ffffff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.text-gradient { + background: linear-gradient(135deg, #FF6B50 0%, #FF9A85 40%, #ffffff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ── Nav Separator ───────────────────────────────── */ + +.nav-separator { + width: 1px; + background: linear-gradient(to bottom, transparent, rgba(255, 107, 80, 0.4), transparent); +} + +/* ── Social Icon Glow ────────────────────────────── */ + +.social-glow:hover { + box-shadow: 0 8px 24px rgba(255, 107, 80, 0.25); + border-color: rgba(255, 107, 80, 0.4) !important; +} + +/* ── Animated Logo ──────────────────────────────── */ + +.logo-animated { + --logo-glow: rgba(231, 229, 228, 0.3); + position: relative; + display: inline-flex; + flex-direction: column; + font-family: var(--font-mono); + font-size: 11px; + line-height: 1; + font-weight: 400; + color: var(--color-c47-text); + white-space: pre; + user-select: none; + letter-spacing: -0.08em; + cursor: pointer; + text-shadow: 0 0 8px rgba(231, 229, 228, 0.15); +} + +.logo-animated__glyph { + display: flex; + flex-direction: column; + position: relative; + z-index: 1; +} + +.logo-animated__line { + display: flex; + height: 1.1em; +} + +.logo-animated__char { + display: inline-block; + min-width: 0.55em; + text-align: center; + opacity: 0.3; + transition: opacity 0.1s ease-out; +} + +.logo-animated.is-animating .logo-animated__char:not(.is-locked) { + opacity: 0.5; +} + +.logo-animated__char.is-locked { + opacity: 1; +} + +.logo-animated__char.is-flashing { + text-shadow: 0 0 6px var(--logo-glow), 0 0 12px rgba(245, 158, 11, 0.25); +} + +.logo-animated.is-complete .logo-animated__glyph { + text-shadow: 0 0 1px var(--logo-glow), 0 0 8px var(--logo-glow); + animation: logo-breathe 4s ease-in-out infinite; + animation-delay: 1s; +} + +@keyframes logo-breathe { + 0%, to { + text-shadow: 0 0 1px var(--logo-glow), 0 0 8px var(--logo-glow); + } + 50% { + text-shadow: 0 0 2px var(--logo-glow), 0 0 12px var(--logo-glow), 0 0 20px rgba(231, 229, 228, 0.18); + } +} + +@media (prefers-reduced-motion: reduce) { + .logo-animated__char { + opacity: 1 !important; + transition: none; + } + .logo-animated__char.is-flashing { + color: inherit; + text-shadow: none; + } + .logo-animated.is-complete .logo-animated__glyph { + animation: none; + } +} + +/* ── View Transitions ────────────────────────────── */ + +@view-transition { navigation: auto; } +::view-transition-old(root), +::view-transition-new(root) { + animation-duration: 0.4s; + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..1a6189c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,54 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import localFont from "next/font/local"; +import { Inter, JetBrains_Mono } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const satoshi = localFont({ + src: [ + { path: "../fonts/Satoshi-Light.woff2", weight: "300", style: "normal" }, + { path: "../fonts/Satoshi-Regular.woff2", weight: "400", style: "normal" }, + { path: "../fonts/Satoshi-Medium.woff2", weight: "500", style: "normal" }, + { path: "../fonts/Satoshi-Bold.woff2", weight: "700", style: "normal" }, + { path: "../fonts/Satoshi-Black.woff2", weight: "900", style: "normal" }, + ], + variable: "--font-satoshi", + display: "swap", +}); + +const inter = Inter({ subsets: ["latin"], + variable: "--font-inter", + display: "swap", }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], + variable: "--font-jetbrains-mono", + display: "swap", }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + other: { + "view-transition": "same-origin", + }, + title: "channel47 — Turn Reddit threads and Amazon reviews into creative strategy", + description: + "A free Claude Code plugin that extracts real customer language from public data and turns it into personas and ad angles you can use today.", + metadataBase: new URL("https://channel47.dev"), + openGraph: { + title: "channel47 — Turn Reddit threads and Amazon reviews into creative strategy", + description: + "A free Claude Code plugin that extracts real customer language from public data and turns it into personas and ad angles you can use today.", + url: "https://channel47.dev", + siteName: "channel47", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "channel47", + description: + "A free Claude Code plugin that extracts real customer language from public data and turns it into personas and ad angles you can use today.", + }, }; export default function RootLayout({ @@ -25,7 +59,7 @@ export default function RootLayout({ return ( {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..bc3d36d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,275 @@ -import Image from "next/image"; +import ScrollReveal from "@/components/ScrollReveal"; +import TopNavigation from "@/components/TopNavigation"; +import FloatingBottomNav from "@/components/FloatingBottomNav"; +import HeroSection from "@/components/HeroSection"; +import SectionHeader from "@/components/SectionHeader"; +import StepCard from "@/components/StepCard"; +import TerminalQuoteWindow from "@/components/TerminalQuoteWindow"; +import SkillCard from "@/components/SkillCard"; +import ProofCard from "@/components/ProofCard"; +import FloatingGlowOrb from "@/components/FloatingGlowOrb"; +import CTAButton from "@/components/CTAButton"; +import NewsletterForm from "@/components/NewsletterForm"; +import FooterHero from "@/components/FooterHero"; +import SocialIconLink from "@/components/SocialIconLink"; export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - + + + + +

+ {/* ── Hero ──────────────────────────────────────────── */} + + + {/* ── How It Works + Terminal Preview ────────────────── */} +
+ + +

+ One command.{" "} + + 30 seconds. Real customer language ready to deploy. + +

+ +
+ + {/* Terminal preview card */} +
- Learning - {" "} - center. -

-
- +
+ + {/* ── Skills Gallery ────────────────────────────────── */} +
+
+

+ The Skills +

+ + Claude Code Plugin — 2024 + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* ── Setup Section ($750) ──────────────────────────── */} +
+
- Vercel logomark + +
+
+
+ + For Teams + +
+ +
+
+

+ Want this running for your team{" "} + by end of day? +

+

+ I will install the plugin, configure it for your accounts, + and walk your team through the first research run. You will + have personas and angles for your first product before our + call ends. +

+
+
+
+ + $750 + + + one-time setup + +
+ +

+ One call. Done by end of day. +

+
+
+
+
+
+ + {/* ── Newsletter ────────────────────────────────────── */} +
+
+
+

+ Build Notes +

+

+ What I am building, what is working, what is not. Weekly. +

+
+
+ +
+
+
+
+ + {/* ── Footer ──────────────────────────────────────────── */} +
+ +
+

+ © 2026 channel47.dev — Built by ctrlswing. All rights reserved. +

+
+ + Privacy Policy + + + Terms of Service + +
+
+
); } diff --git a/src/components/AnimatedLogo.tsx b/src/components/AnimatedLogo.tsx new file mode 100644 index 0000000..26c32e4 --- /dev/null +++ b/src/components/AnimatedLogo.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { useRef, useEffect, useCallback } from 'react'; + +const GLYPHS = ['█','▓','▒','░','▀','▄','▌','▐','■','□','▪','▫','◼','◻','▰','▱']; +const LINES = ['█ █ ▀▀█', '▀▀█ █']; + +function randomGlyph() { + return GLYPHS[Math.floor(Math.random() * GLYPHS.length)]; +} + +export default function AnimatedLogo({ delay = 0 }: { delay?: number }) { + const rootRef = useRef(null); + const startedRef = useRef(false); + const replayingRef = useRef(false); + + const animate = useCallback((isReplay = false) => { + const root = rootRef.current; + if (!root) return; + + const chars = root.querySelectorAll('.logo-animated__char'); + const targets: string[] = []; + chars.forEach(c => targets.push(c.dataset.char || ' ')); + + const locked = new Array(chars.length).fill(false); + + // Reset state + root.classList.remove('is-complete'); + root.classList.add('is-animating'); + if (!isReplay) { + chars.forEach(c => c.classList.remove('is-locked')); + } else { + locked.fill(false); + chars.forEach(c => c.classList.remove('is-locked')); + } + + // Scramble non-space chars + chars.forEach((c, i) => { + if (targets[i] !== ' ') c.textContent = randomGlyph(); + }); + + // Continuous scramble + const scrambleInterval = window.setInterval(() => { + chars.forEach((c, i) => { + if (!locked[i] && targets[i] !== ' ') { + c.textContent = randomGlyph(); + } + }); + }, 50); + + // Lock characters with eased stagger + const nonSpaceIndices = targets + .map((ch, i) => (ch !== ' ' ? i : -1)) + .filter(i => i !== -1) + .sort(() => Math.random() - 0.5); + + const duration = isReplay ? 400 : 800; + const baseDelay = isReplay ? 100 : 300; + const timeouts: number[] = []; + + nonSpaceIndices.forEach((charIdx, order) => { + const progress = order / nonSpaceIndices.length; + const eased = 1 - Math.pow(1 - progress, 2); + const t = window.setTimeout(() => { + locked[charIdx] = true; + const el = chars[charIdx]; + el.textContent = targets[charIdx]; + el.classList.add('is-locked'); + el.classList.add('is-flashing'); + setTimeout(() => el.classList.remove('is-flashing'), 100); + + // Last char — complete + if (order === nonSpaceIndices.length - 1) { + setTimeout(() => { + clearInterval(scrambleInterval); + root.classList.add('is-complete'); + replayingRef.current = false; + }, 150); + } + }, baseDelay + eased * duration); + timeouts.push(t); + }); + + return () => { + timeouts.forEach(clearTimeout); + clearInterval(scrambleInterval); + }; + }, []); + + useEffect(() => { + const root = rootRef.current; + if (!root) return; + + const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReduced) { + root.classList.add('is-complete'); + root.querySelectorAll('.logo-animated__char').forEach(c => { + c.classList.add('is-locked'); + }); + return; + } + + const observer = new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.isIntersecting && !startedRef.current) { + startedRef.current = true; + setTimeout(() => animate(false), delay); + observer.disconnect(); + } + }); + }, + { threshold: 0.5 } + ); + + observer.observe(root); + return () => observer.disconnect(); + }, [animate, delay]); + + const handleMouseEnter = useCallback(() => { + if ( + !startedRef.current || + replayingRef.current || + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ) return; + replayingRef.current = true; + animate(true); + }, [animate]); + + return ( + + + + ); +} diff --git a/src/components/CTAButton.tsx b/src/components/CTAButton.tsx new file mode 100644 index 0000000..ffceaeb --- /dev/null +++ b/src/components/CTAButton.tsx @@ -0,0 +1,10 @@ +export default function CTAButton({ href }: { href: string }) { + return ( + + Book a setup call + + ); +} diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx new file mode 100644 index 0000000..fae0c3d --- /dev/null +++ b/src/components/CopyButton.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useState } from "react"; + +export default function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback for older browsers + const textarea = document.createElement("textarea"); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + return ( + + ); +} diff --git a/src/components/FloatingBottomNav.tsx b/src/components/FloatingBottomNav.tsx new file mode 100644 index 0000000..f02fc89 --- /dev/null +++ b/src/components/FloatingBottomNav.tsx @@ -0,0 +1,54 @@ +const navItems = [ + { label: "Skills", icon: "M4 4h6v6H4V4zm10 0h6v6h-6V4zM4 14h6v6H4v-6zm10 0h6v6h-6v-6z", active: true }, + { label: "Global", icon: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 2a8 8 0 0 1 5.29 2H6.71A8 8 0 0 1 12 4zM4 12a8 8 0 0 1 .34-2.3h15.32A8 8 0 0 1 20 12a8 8 0 0 1-.34 2.3H4.34A8 8 0 0 1 4 12zm2.71 6h10.58A8 8 0 0 1 12 20a8 8 0 0 1-5.29-2z", active: false }, + { label: "Plugin", icon: "M4 17l6-6-6-6M12 19h8", active: false }, + { label: "Services", icon: "M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 16.8l-6.2 4.5 2.4-7.4L2 9.4h7.6z", active: false }, + { label: "Updates", icon: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 14.5A2.5 2.5 0 0 1 6.5 12H20M4 4h16v6H4V4z", active: false }, +]; + +export default function FloatingBottomNav() { + return ( +
+
+ {navItems.map((item) => ( + + ))} + +
+ ); +} diff --git a/src/components/FloatingGlowOrb.tsx b/src/components/FloatingGlowOrb.tsx new file mode 100644 index 0000000..c8c90ba --- /dev/null +++ b/src/components/FloatingGlowOrb.tsx @@ -0,0 +1,20 @@ +export default function FloatingGlowOrb() { + return ( + <> +
+
+ + ); +} diff --git a/src/components/FooterHero.tsx b/src/components/FooterHero.tsx new file mode 100644 index 0000000..4b96a41 --- /dev/null +++ b/src/components/FooterHero.tsx @@ -0,0 +1,35 @@ +export default function FooterHero() { + return ( +
+

+ LET’S +
+ TALK. +

+
+ + hello@channel47.dev + +

+ + + + + Available for worldwide collaborations. +

+
+
+ ); +} diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx new file mode 100644 index 0000000..bca3ec5 --- /dev/null +++ b/src/components/HeroSection.tsx @@ -0,0 +1,65 @@ +import CopyButton from "./CopyButton"; +import AnimatedLogo from "./AnimatedLogo"; + +export default function HeroSection() { + return ( +
+
+
+
+ +
+

+ + / + + strategy +

+

+ A free Claude Code plugin that extracts real customer language from + public data and turns it into personas and ad angles you can use today. +

+ +
+
+ $ + + claude plugin marketplace add channel47 + + +
+
+ + + Or have it set up for your team{" "} + + +
+ + + hello@channel47.dev + +
+ ); +} diff --git a/src/components/NewsletterForm.tsx b/src/components/NewsletterForm.tsx new file mode 100644 index 0000000..9a13946 --- /dev/null +++ b/src/components/NewsletterForm.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useState, type FormEvent } from "react"; + +export default function NewsletterForm() { + const [email, setEmail] = useState(""); + const [status, setStatus] = useState< + "idle" | "loading" | "success" | "error" + >("idle"); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!email) return; + + setStatus("loading"); + + try { + await new Promise((resolve) => setTimeout(resolve, 800)); + setStatus("success"); + setEmail(""); + } catch { + setStatus("error"); + } + }; + + if (status === "success") { + return ( +

+ You’re in. Watch your inbox. +

+ ); + } + + return ( +
+ setEmail(e.target.value)} + placeholder="Email address" + aria-label="Email address" + className="newsletter-input flex-1 bg-transparent border-b-2 border-[#333333] text-zinc-100 px-0 py-3 outline-none placeholder:text-zinc-600 text-sm" + /> + + {status === "error" && ( +

+ Something went wrong. Try again. +

+ )} +
+ ); +} diff --git a/src/components/ProofCard.tsx b/src/components/ProofCard.tsx new file mode 100644 index 0000000..f1cee95 --- /dev/null +++ b/src/components/ProofCard.tsx @@ -0,0 +1,50 @@ +export default function ProofCard() { + return ( +
+
+
+

+ “Built from running creative strategy across 25+ DTC brands. + Every framework was used in production before it became a + skill.” +

+

+ // Used by marketers managing paid media across Google, Meta, and + Microsoft Ads. +

+
+
+
+
+

+ BUILT FROM REAL WORK +

+

+ 25+ DTC Brands / Production +

+
+
+ + + + +
+
+
+ ); +} diff --git a/src/components/ScrollReveal.tsx b/src/components/ScrollReveal.tsx new file mode 100644 index 0000000..f8dcee5 --- /dev/null +++ b/src/components/ScrollReveal.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useEffect } from "react"; + +export default function ScrollReveal() { + useEffect(() => { + const initializeObservers = () => { + try { + if (!("IntersectionObserver" in window)) { + document.querySelectorAll(".reveal").forEach((el) => el.classList.add("visible")); + document.querySelectorAll(".terminal-container").forEach((el) => { + const lines = el.querySelector(".terminal-lines"); + if (lines) lines.classList.add("terminal-active"); + }); + return; + } + + const revealObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry && entry.isIntersecting && entry.target) { + entry.target.classList.add("visible"); + } + }); + }, + { threshold: 0.1, rootMargin: "0px 0px -40px 0px" } + ); + + const terminalObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry && entry.isIntersecting && entry.target) { + const lines = entry.target.querySelector(".terminal-lines"); + if (lines) lines.classList.add("terminal-active"); + } + }); + }, + { threshold: 0.5 } + ); + + const observe = () => { + document.querySelectorAll(".reveal").forEach((el) => revealObserver.observe(el)); + document.querySelectorAll(".terminal-container").forEach((el) => terminalObserver.observe(el)); + }; + + observe(); + setTimeout(observe, 800); + setTimeout(observe, 2000); + + return () => { + revealObserver.disconnect(); + terminalObserver.disconnect(); + }; + } catch (err) { + console.warn("ScrollReveal init failed gracefully:", err); + document.querySelectorAll(".reveal").forEach((el) => el.classList.add("visible")); + } + }; + + const cleanup = initializeObservers(); + return cleanup; + }, []); + + return null; +} diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx new file mode 100644 index 0000000..2fd1aa3 --- /dev/null +++ b/src/components/SectionHeader.tsx @@ -0,0 +1,13 @@ +export default function SectionHeader({ label }: { label: string }) { + return ( +
+
+ + {label} + +
+ ); +} diff --git a/src/components/SkillCard.tsx b/src/components/SkillCard.tsx new file mode 100644 index 0000000..ba7c906 --- /dev/null +++ b/src/components/SkillCard.tsx @@ -0,0 +1,56 @@ +import Image from "next/image"; + +export default function SkillCard({ + title, + category, + description, + imageSrc, +}: { + title: string; + category: string; + description: string; + imageSrc: string; +}) { + return ( +
+
+ {title} +
+
+
+
+

+ {title} +

+

+ {category} +

+
+
+ + + + +
+
+

+ {description} +

+
+ ); +} diff --git a/src/components/SocialIconLink.tsx b/src/components/SocialIconLink.tsx new file mode 100644 index 0000000..3ec83c4 --- /dev/null +++ b/src/components/SocialIconLink.tsx @@ -0,0 +1,21 @@ +export default function SocialIconLink({ + href, + label, + icon, +}: { + href: string; + label: string; + icon: React.ReactNode; +}) { + return ( + + {icon} + + ); +} diff --git a/src/components/StepCard.tsx b/src/components/StepCard.tsx new file mode 100644 index 0000000..7cfc18b --- /dev/null +++ b/src/components/StepCard.tsx @@ -0,0 +1,24 @@ +export default function StepCard({ + stepNumber, + title, + description, +}: { + stepNumber: string; + title: string; + description: string; +}) { + return ( +
+ + {stepNumber} + +
+

{title}

+

{description}

+
+
+ ); +} diff --git a/src/components/TerminalQuoteWindow.tsx b/src/components/TerminalQuoteWindow.tsx new file mode 100644 index 0000000..6af28b6 --- /dev/null +++ b/src/components/TerminalQuoteWindow.tsx @@ -0,0 +1,56 @@ +export default function TerminalQuoteWindow() { + return ( +
+
+
+
+
+ + creative-strategist extract --raw + +
+
+
+ [fire-3] +
+ + “Marketing has always been the hardest part for us solo + founders. You can build an amazing product but if nobody knows + about it, none of that matters.” + +
+ + — Reddit, r/SaaS | Journey: Problem-aware + +
+
+
+ [fire-2] +
+ + “I was burning hours every week hunting for new + tools.” + +
+ + — PPC.io | Journey: Problem-aware + +
+
+
+ [fire-3] +
+ + “I found $3,400/month in wasted spend. In one + prompt.” + +
+ + — Adspirer | Journey: Post-purchase + +
+
+
+
+ ); +} diff --git a/src/components/TopNavigation.tsx b/src/components/TopNavigation.tsx new file mode 100644 index 0000000..f5231d2 --- /dev/null +++ b/src/components/TopNavigation.tsx @@ -0,0 +1,66 @@ +import AnimatedLogo from "./AnimatedLogo"; + +export default function TopNavigation() { + return ( + + ); +} diff --git a/src/fonts/Satoshi-Black.woff2 b/src/fonts/Satoshi-Black.woff2 new file mode 100644 index 0000000..64492d5 Binary files /dev/null and b/src/fonts/Satoshi-Black.woff2 differ diff --git a/src/fonts/Satoshi-Bold.woff2 b/src/fonts/Satoshi-Bold.woff2 new file mode 100644 index 0000000..0a8db7a Binary files /dev/null and b/src/fonts/Satoshi-Bold.woff2 differ diff --git a/src/fonts/Satoshi-Light.woff2 b/src/fonts/Satoshi-Light.woff2 new file mode 100644 index 0000000..cf18cd4 Binary files /dev/null and b/src/fonts/Satoshi-Light.woff2 differ diff --git a/src/fonts/Satoshi-Medium.woff2 b/src/fonts/Satoshi-Medium.woff2 new file mode 100644 index 0000000..ffd0ac9 Binary files /dev/null and b/src/fonts/Satoshi-Medium.woff2 differ diff --git a/src/fonts/Satoshi-Regular.woff2 b/src/fonts/Satoshi-Regular.woff2 new file mode 100644 index 0000000..81c40ab Binary files /dev/null and b/src/fonts/Satoshi-Regular.woff2 differ