chore: overhaul infrastructure and integrate @mintel packages
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
- Restructure to pnpm monorepo (site moved to apps/web) - Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config - Implement Docker service architecture (Varnish, Directus, Gatekeeper) - Setup environment-aware Gitea Actions deployment
This commit is contained in:
221
apps/web/src/components/Landing/AbstractLines.tsx
Normal file
221
apps/web/src/components/Landing/AbstractLines.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface LineProps {
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const HeroLines: React.FC<LineProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={`absolute pointer-events-none ${className}`} viewBox="0 0 800 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path
|
||||
d="M-100 300 C 100 300, 200 100, 400 100 C 600 100, 700 500, 900 500"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
transition={{ duration: 2, delay: delay, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M-100 350 C 100 350, 200 150, 400 150 C 600 150, 700 550, 900 550"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
className="text-slate-100"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
transition={{ duration: 2.5, delay: delay + 0.2, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Animated Pulses */}
|
||||
<motion.circle r="3" fill="currentColor" className="text-slate-300">
|
||||
<animateMotion
|
||||
dur="6s"
|
||||
repeatCount="indefinite"
|
||||
path="M-100 300 C 100 300, 200 100, 400 100 C 600 100, 700 500, 900 500"
|
||||
/>
|
||||
</motion.circle>
|
||||
<motion.circle r="3" fill="currentColor" className="text-slate-200">
|
||||
<animateMotion
|
||||
dur="8s"
|
||||
repeatCount="indefinite"
|
||||
path="M-100 350 C 100 350, 200 150, 400 150 C 600 150, 700 550, 900 550"
|
||||
/>
|
||||
</motion.circle>
|
||||
|
||||
{/* Nodes */}
|
||||
<motion.circle cx="400" cy="100" r="4" className="fill-slate-200"
|
||||
initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: delay + 1, duration: 0.5 }} />
|
||||
<motion.circle cx="400" cy="150" r="4" className="fill-slate-100"
|
||||
initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: delay + 1.2, duration: 0.5 }} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GridLines: React.FC<LineProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={`absolute pointer-events-none ${className}`} viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeWidth="0.5" className="text-slate-100" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
|
||||
{/* Highlighted Path */}
|
||||
<motion.path
|
||||
d="M 40 40 L 120 40 L 120 120 L 200 120"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay: delay }}
|
||||
/>
|
||||
|
||||
{/* Active Cells */}
|
||||
<motion.rect x="120" y="40" width="40" height="40" className="fill-slate-50"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: [0, 0.5, 0] }} transition={{ duration: 3, repeat: Infinity, repeatDelay: 2 }} />
|
||||
<motion.rect x="160" y="80" width="40" height="40" className="fill-slate-50"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: [0, 0.5, 0] }} transition={{ duration: 4, repeat: Infinity, repeatDelay: 1 }} />
|
||||
|
||||
<motion.circle cx="200" cy="120" r="3" className="fill-slate-400"
|
||||
initial={{ scale: 0 }} whileInView={{ scale: 1 }} viewport={{ once: true }} transition={{ delay: delay + 1.5 }} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const FlowLines: React.FC<LineProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={`absolute pointer-events-none ${className}`} viewBox="0 0 600 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path
|
||||
d="M 0 100 H 100 C 150 100, 150 50, 200 50 H 300"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay: delay }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 0 100 H 100 C 150 100, 150 150, 200 150 H 300"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay: delay + 0.2 }}
|
||||
/>
|
||||
|
||||
{/* Pulse */}
|
||||
<motion.circle r="2" fill="currentColor" className="text-slate-400">
|
||||
<animateMotion
|
||||
dur="4s"
|
||||
repeatCount="indefinite"
|
||||
path="M 0 100 H 100 C 150 100, 150 50, 200 50 H 300"
|
||||
/>
|
||||
</motion.circle>
|
||||
|
||||
<motion.rect x="300" y="30" width="80" height="40" rx="8" className="stroke-slate-300 fill-white" strokeWidth="1"
|
||||
initial={{ opacity: 0, x: 280 }} whileInView={{ opacity: 1, x: 300 }} viewport={{ once: true }} transition={{ delay: delay + 1 }} />
|
||||
|
||||
<motion.rect x="300" y="130" width="80" height="40" rx="8" className="stroke-slate-300 fill-white" strokeWidth="1"
|
||||
initial={{ opacity: 0, x: 280 }} whileInView={{ opacity: 1, x: 300 }} viewport={{ once: true }} transition={{ delay: delay + 1.2 }} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CirclePattern: React.FC<LineProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={`absolute pointer-events-none ${className}`} viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.circle cx="200" cy="200" r="100" stroke="currentColor" strokeWidth="1" className="text-slate-100"
|
||||
initial={{ scale: 0.8, opacity: 0 }} whileInView={{ scale: 1, opacity: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: delay }} />
|
||||
<motion.circle cx="200" cy="200" r="150" stroke="currentColor" strokeWidth="1" className="text-slate-50"
|
||||
initial={{ scale: 0.8, opacity: 0 }} whileInView={{ scale: 1, opacity: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: delay + 0.2 }} />
|
||||
<motion.circle cx="200" cy="200" r="50" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ scale: 0.8, opacity: 0 }} whileInView={{ scale: 1, opacity: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: delay + 0.4 }} />
|
||||
|
||||
{/* Rotating Ring */}
|
||||
<motion.circle cx="200" cy="200" r="120" stroke="currentColor" strokeWidth="1" strokeDasharray="10 10" className="text-slate-200"
|
||||
animate={{ rotate: 360 }} transition={{ duration: 20, repeat: Infinity, ease: "linear" }} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const ServicesFlow: React.FC<LineProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={`absolute pointer-events-none ${className}`} viewBox="0 0 1000 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Path connecting the 3 steps */}
|
||||
<motion.path
|
||||
d="M 100 100 L 900 100"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeDasharray="8 8"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 2, delay: delay }}
|
||||
/>
|
||||
|
||||
{/* Animated pulse moving along the line */}
|
||||
<motion.circle r="5" fill="currentColor" className="text-slate-900">
|
||||
<animateMotion
|
||||
dur="3s"
|
||||
repeatCount="indefinite"
|
||||
path="M 100 100 L 900 100"
|
||||
/>
|
||||
</motion.circle>
|
||||
|
||||
{/* Second pulse with delay */}
|
||||
<motion.circle r="5" fill="currentColor" className="text-slate-900">
|
||||
<animateMotion
|
||||
dur="3s"
|
||||
begin="1.5s"
|
||||
repeatCount="indefinite"
|
||||
path="M 100 100 L 900 100"
|
||||
/>
|
||||
</motion.circle>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComparisonLines: React.FC<LineProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={`absolute pointer-events-none ${className}`} viewBox="0 0 100 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path
|
||||
d="M 50 0 V 400"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeDasharray="4 4"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay: delay }}
|
||||
/>
|
||||
<motion.circle r="5" fill="currentColor" className="text-slate-900">
|
||||
<animateMotion
|
||||
dur="4s"
|
||||
repeatCount="indefinite"
|
||||
path="M 50 0 V 400"
|
||||
/>
|
||||
</motion.circle>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const ConnectorStart: React.FC<LineProps> = ({ className = "", delay = 0 }) => null;
|
||||
export const ConnectorBranch: React.FC<LineProps> = ({ className = "", delay = 0 }) => null;
|
||||
export const ConnectorSplit: React.FC<LineProps> = ({ className = "", delay = 0 }) => null;
|
||||
export const ConnectorEnd: React.FC<LineProps> = ({ className = "", delay = 0 }) => null;
|
||||
50
apps/web/src/components/Landing/ComparisonRow.tsx
Normal file
50
apps/web/src/components/Landing/ComparisonRow.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { Reveal } from '../Reveal';
|
||||
import { Label, H3, LeadText } from '../Typography';
|
||||
|
||||
interface ComparisonRowProps {
|
||||
negativeLabel: string;
|
||||
negativeText: React.ReactNode;
|
||||
positiveLabel: string;
|
||||
positiveText: React.ReactNode;
|
||||
reverse?: boolean;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const ComparisonRow: React.FC<ComparisonRowProps> = ({
|
||||
negativeLabel,
|
||||
negativeText,
|
||||
positiveLabel,
|
||||
positiveText,
|
||||
reverse = false,
|
||||
delay = 0,
|
||||
}) => {
|
||||
return (
|
||||
<Reveal delay={delay}>
|
||||
<div className={`flex flex-col ${reverse ? 'md:flex-row-reverse' : 'md:flex-row'} gap-8 md:gap-12 items-center`}>
|
||||
<div className="flex-1 p-8 md:p-10 bg-slate-50/50 rounded-2xl text-slate-400 border border-transparent w-full">
|
||||
<Label className="mb-4 line-through decoration-slate-200">
|
||||
{negativeLabel}
|
||||
</Label>
|
||||
<LeadText className="line-through decoration-slate-200 leading-snug">
|
||||
{negativeText}
|
||||
</LeadText>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0">
|
||||
<ArrowRight className={`w-6 h-6 text-slate-200 hidden md:block ${reverse ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-8 md:p-10 border border-slate-100 rounded-2xl bg-white hover:border-slate-200 transition-all duration-500 hover:shadow-xl hover:shadow-slate-100/50 w-full">
|
||||
<Label className="text-slate-900 mb-4">
|
||||
{positiveLabel}
|
||||
</Label>
|
||||
<H3 className="text-2xl md:text-3xl">
|
||||
{positiveText}
|
||||
</H3>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
};
|
||||
3
apps/web/src/components/Landing/ConceptIllustrations.tsx
Normal file
3
apps/web/src/components/Landing/ConceptIllustrations.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export * from './Illustrations';
|
||||
313
apps/web/src/components/Landing/ExplanatoryIllustrations.tsx
Normal file
313
apps/web/src/components/Landing/ExplanatoryIllustrations.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface IllustrationProps {
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const DirectCommunication: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.circle
|
||||
cx="20" cy="60" r="4"
|
||||
className="fill-slate-400"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="100" cy="60" r="4"
|
||||
className="fill-slate-900"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.2 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 24 60 H 96"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay: delay + 0.4 }}
|
||||
/>
|
||||
<motion.circle r="2" className="fill-slate-400">
|
||||
<animateMotion
|
||||
dur="3s"
|
||||
repeatCount="indefinite"
|
||||
path="M 24 60 H 96"
|
||||
/>
|
||||
</motion.circle>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FastPrototyping: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.rect
|
||||
x="20" y="30" width="80" height="60" rx="4"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
whileInView={{ pathLength: 1, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay: delay }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 20 45 H 100"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 0.5 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 45 55 L 75 65 L 45 75 Z"
|
||||
className="fill-slate-200 stroke-slate-300"
|
||||
strokeWidth="1"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.8 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 85 37 H 90"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CleanCode: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path
|
||||
d="M 30 40 H 70"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 30 55 H 90"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 0.2 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 30 70 H 60"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 0.4 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 30 85 H 80"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 0.6 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 85 45 L 95 55 L 110 35"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
whileInView={{ pathLength: 1, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 1 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FixedPrice: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.rect
|
||||
x="35" y="35" width="50" height="50" rx="2"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
whileInView={{ pathLength: 1, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay: delay }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 45 50 H 75 M 45 60 H 75 M 45 70 H 65"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: delay + 0.5 }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="85" cy="35" r="12"
|
||||
className="fill-white stroke-slate-900"
|
||||
strokeWidth="1"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 10, delay: delay + 1 }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 80 35 L 83 38 L 90 31"
|
||||
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: delay + 1.3 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const WebsitesIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.rect x="20" y="30" width="80" height="60" rx="2" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay }} />
|
||||
<motion.path d="M 20 42 H 100" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.5 }} />
|
||||
<motion.rect x="30" y="50" width="25" height="30" rx="1" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.8 }} />
|
||||
<motion.path d="M 65 55 H 90 M 65 65 H 90 M 65 75 H 80" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 1 }} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SystemsIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path d="M 40 30 L 20 40 V 80 L 40 90 L 60 80 V 40 L 40 30 Z" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay }} />
|
||||
<motion.path d="M 80 50 L 60 60 V 100 L 80 110 L 100 100 V 60 L 80 50 Z" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: delay + 0.3 }} />
|
||||
<motion.path d="M 60 60 L 40 50" stroke="currentColor" strokeWidth="1" strokeDasharray="2 2" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.8 }} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AutomationIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.circle cx="45" cy="45" r="15" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay }} />
|
||||
<motion.circle cx="75" cy="75" r="15" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: delay + 0.2 }} />
|
||||
<motion.path d="M 55 55 L 65 65" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.5 }} />
|
||||
<motion.path d="M 62 58 L 65 65 L 58 62" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.7 }} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MinimalistArchitect: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path
|
||||
d="M 40 160 V 60 L 100 30 L 160 60 V 160"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay: delay }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 40 100 H 160 M 100 30 V 160"
|
||||
stroke="currentColor" strokeWidth="0.5"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay: delay + 0.5 }}
|
||||
/>
|
||||
<motion.rect
|
||||
x="85" y="120" width="30" height="40"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1, duration: 0.5 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DifferenceIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Messy Path */}
|
||||
<motion.path
|
||||
d="M 20 50 C 30 20, 40 80, 50 50 C 60 20, 70 80, 80 50"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 2, delay }}
|
||||
/>
|
||||
{/* Arrow to clean side */}
|
||||
<motion.path
|
||||
d="M 90 50 H 110"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ delay: delay + 1 }}
|
||||
/>
|
||||
{/* Clean Path */}
|
||||
<motion.path
|
||||
d="M 120 50 H 180"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-slate-900"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay: delay + 1.5 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TargetGroupIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.circle cx="60" cy="40" r="20" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ scale: 0 }} whileInView={{ scale: 1 }} viewport={{ once: true }} transition={{ delay }} />
|
||||
<motion.path d="M 30 90 C 30 70, 90 70, 90 90" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.3 }} />
|
||||
<motion.path d="M 50 20 L 60 10 L 70 20" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.6 }} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ContactIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.rect x="20" y="35" width="80" height="50" rx="2" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay }} />
|
||||
<motion.path d="M 20 35 L 60 65 L 100 35" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 0.8, delay: delay + 0.5 }} />
|
||||
<motion.circle cx="90" cy="35" r="5" className="fill-green-500"
|
||||
initial={{ scale: 0 }} whileInView={{ scale: 1 }} viewport={{ once: true }} transition={{ type: "spring", delay: delay + 1.2 }} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PromiseSectionIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path d="M 20 60 L 50 90 L 100 30" stroke="currentColor" strokeWidth="2" className="text-slate-300"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay }} />
|
||||
<motion.path d="M 40 50 H 80 M 40 70 H 70" stroke="currentColor" strokeWidth="1" className="text-slate-100"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.5 }} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ServicesSectionIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.rect x="20" y="20" width="80" height="80" rx="4" stroke="currentColor" strokeWidth="1" className="text-slate-200"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ duration: 1, delay }} />
|
||||
<motion.circle cx="60" cy="60" r="20" stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
initial={{ scale: 0 }} whileInView={{ scale: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.5 }} />
|
||||
<motion.path d="M 60 40 V 80 M 40 60 H 80" stroke="currentColor" strokeWidth="1" className="text-slate-100"
|
||||
initial={{ pathLength: 0 }} whileInView={{ pathLength: 1 }} viewport={{ once: true }} transition={{ delay: delay + 0.8 }} />
|
||||
</svg>
|
||||
);
|
||||
32
apps/web/src/components/Landing/FeatureCard.tsx
Normal file
32
apps/web/src/components/Landing/FeatureCard.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { Reveal } from '../Reveal';
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const FeatureCard: React.FC<FeatureCardProps> = ({ icon: Icon, title, description, delay = 0 }) => {
|
||||
return (
|
||||
<Reveal delay={delay}>
|
||||
<div className="p-8 md:p-10 border border-slate-100 rounded-2xl hover:border-slate-200 transition-all duration-500 group bg-white hover:shadow-xl hover:shadow-slate-100/50 relative overflow-hidden">
|
||||
{/* Animated Top Line */}
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-slate-900 scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left z-20"></div>
|
||||
|
||||
{/* Decorative Ring */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 border border-slate-100 rounded-full -mr-16 -mt-16 group-hover:scale-150 transition-transform duration-700"></div>
|
||||
|
||||
<Icon className="w-8 h-8 mb-6 text-slate-300 group-hover:text-slate-900 group-hover:scale-110 group-hover:rotate-3 transition-all duration-500 relative z-10" />
|
||||
<h3 className="text-xl md:text-2xl font-bold text-slate-900 mb-3 tracking-tight relative z-10">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-lg md:text-xl text-slate-500 font-serif italic leading-relaxed relative z-10">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
};
|
||||
33
apps/web/src/components/Landing/HeroItem.tsx
Normal file
33
apps/web/src/components/Landing/HeroItem.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { Reveal } from '../Reveal';
|
||||
|
||||
interface HeroItemProps {
|
||||
number: string;
|
||||
title: string;
|
||||
description: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const HeroItem: React.FC<HeroItemProps> = ({ number, title, description, delay = 0 }) => {
|
||||
return (
|
||||
<Reveal delay={delay}>
|
||||
<div className="group flex gap-6 md:gap-8 p-6 md:p-8 border border-transparent hover:border-slate-100 rounded-2xl transition-all duration-500 hover:bg-slate-50/50 relative overflow-hidden">
|
||||
{/* Animated Bottom Line */}
|
||||
<div className="absolute bottom-0 left-8 right-8 h-px bg-slate-200 scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left"></div>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-slate-50/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<span className="text-xs font-bold uppercase tracking-[0.2em] text-slate-300 group-hover:text-slate-900 transition-colors pt-1.5 relative z-10">
|
||||
{number}
|
||||
</span>
|
||||
<div className="space-y-2 relative z-10">
|
||||
<h3 className="text-2xl md:text-4xl font-bold text-slate-900 tracking-tight group-hover:tracking-tighter transition-all duration-500">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-lg md:text-xl text-slate-400 font-serif italic leading-snug group-hover:text-slate-600 transition-colors duration-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptAutomation: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.g
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
|
||||
style={{ originX: "40px", originY: "60px" }}
|
||||
>
|
||||
<path d="M 40 45 L 50 60 L 40 75 L 30 60 Z" className="fill-slate-300" />
|
||||
</motion.g>
|
||||
<motion.g
|
||||
animate={{ rotate: -360 }}
|
||||
transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
|
||||
style={{ originX: "75px", originY: "65px" }}
|
||||
>
|
||||
<path d="M 75 50 L 85 65 L 75 80 L 65 65 Z" className="fill-slate-500" />
|
||||
</motion.g>
|
||||
<motion.path
|
||||
d="M 10 60 H 110"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
strokeDasharray="4 4"
|
||||
animate={{ strokeDashoffset: [0, -20] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptCode: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{[40, 55, 70, 85].map((y, i) => (
|
||||
<motion.path
|
||||
key={y}
|
||||
d={`M 25 ${y} H ${25 + ((i * 17) % 50) + 20}`}
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
className="text-slate-400"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: [0, 1, 1, 0] }}
|
||||
transition={{ duration: 4, repeat: Infinity, delay: i * 0.2 + delay }}
|
||||
/>
|
||||
))}
|
||||
<motion.path
|
||||
d="M 90 40 L 100 50 L 115 30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-slate-900"
|
||||
animate={{ opacity: [0, 1, 1, 0], scale: [0.8, 1, 1, 0.8] }}
|
||||
transition={{ duration: 4, repeat: Infinity, delay: 1.5 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptCommunication: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="20" cy="60" r="6" className="fill-slate-200 stroke-slate-300" strokeWidth="1" />
|
||||
<circle cx="100" cy="60" r="6" className="fill-slate-900" />
|
||||
<path d="M 26 60 H 94" stroke="currentColor" strokeWidth="1" className="text-slate-300" strokeDasharray="4 4" />
|
||||
<motion.path
|
||||
d="M 26 60 H 94"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-slate-400"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: [0, 1, 1, 0], opacity: [0, 1, 1, 0] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay }}
|
||||
/>
|
||||
<motion.circle r="3" className="fill-slate-900">
|
||||
<animateMotion
|
||||
dur="3s"
|
||||
repeatCount="indefinite"
|
||||
path="M 26 60 H 94"
|
||||
/>
|
||||
</motion.circle>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptMessy: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.path
|
||||
d="M 20 60 C 30 20, 40 100, 50 60 C 60 20, 70 100, 80 60 C 90 20, 100 100, 110 60"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-500"
|
||||
animate={{ strokeDashoffset: [0, 20] }}
|
||||
strokeDasharray="4 4"
|
||||
transition={{ duration: 5, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 20 40 L 100 80 M 20 80 L 100 40"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-200 opacity-50"
|
||||
animate={{ opacity: [0.2, 0.5, 0.2] }} transition={{ duration: 3, repeat: Infinity }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptPrice: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="30" y="30" width="60" height="70" rx="2" stroke="currentColor" strokeWidth="1" className="text-slate-400" />
|
||||
<motion.path
|
||||
d="M 40 50 H 80 M 40 65 H 80 M 40 80 H 60"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: [0, 1, 1, 0] }}
|
||||
transition={{ duration: 5, repeat: Infinity, delay }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="85" cy="35" r="15"
|
||||
className="fill-white stroke-slate-900"
|
||||
strokeWidth="1"
|
||||
animate={{ y: [0, -5, 0], rotate: [0, 10, 0] }}
|
||||
transition={{ duration: 4, repeat: Infinity }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptPrototyping: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="15" y="25" width="90" height="70" rx="4" stroke="currentColor" strokeWidth="1" className="text-slate-400" />
|
||||
<path d="M 15 40 H 105" stroke="currentColor" strokeWidth="1" className="text-slate-300" />
|
||||
<motion.rect
|
||||
x="25" y="50" width="40" height="8" rx="1"
|
||||
className="fill-slate-300"
|
||||
animate={{ width: [0, 40, 40, 0] }}
|
||||
transition={{ duration: 4, repeat: Infinity, delay }}
|
||||
/>
|
||||
<motion.rect
|
||||
x="25" y="65" width="60" height="8" rx="1"
|
||||
className="fill-slate-200"
|
||||
animate={{ width: [0, 60, 60, 0] }}
|
||||
transition={{ duration: 4, repeat: Infinity, delay: 0.5 }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="85" cy="75" r="10"
|
||||
className="fill-slate-900"
|
||||
animate={{ scale: [0.8, 1.1, 0.8] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptSystem: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.circle cx="60" cy="60" r="15" className="fill-slate-900"
|
||||
animate={{ scale: [1, 1.1, 1] }} transition={{ duration: 4, repeat: Infinity }} />
|
||||
{[0, 72, 144, 216, 288].map((angle, i) => {
|
||||
const x = 60 + Math.cos((angle * Math.PI) / 180) * 40;
|
||||
const y = 60 + Math.sin((angle * Math.PI) / 180) * 40;
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<motion.line
|
||||
x1="60" y1="60" x2={x} y2={y}
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-400"
|
||||
animate={{ strokeDashoffset: [0, 10] }}
|
||||
strokeDasharray="2 2"
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx={x} cy={y} r="6"
|
||||
className="fill-white stroke-slate-300"
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 3, repeat: Infinity, delay: i * 0.4 }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptTarget: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.circle
|
||||
cx="60" cy="60" r="50"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-300"
|
||||
animate={{ scale: [1, 1.05, 1] }}
|
||||
transition={{ duration: 4, repeat: Infinity }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="60" cy="60" r="30"
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-400"
|
||||
/>
|
||||
<motion.circle
|
||||
cx="60" cy="60" r="10"
|
||||
className="fill-slate-900"
|
||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ConceptWebsite: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="20" width="100" height="80" rx="4" stroke="currentColor" strokeWidth="1" className="text-slate-400" />
|
||||
<motion.rect
|
||||
x="20" y="35" width="80" height="15" rx="2"
|
||||
className="fill-slate-200"
|
||||
animate={{ opacity: [0.3, 0.6, 0.3] }}
|
||||
transition={{ duration: 3, repeat: Infinity }}
|
||||
/>
|
||||
<motion.g
|
||||
animate={{ y: [0, 10, 0] }}
|
||||
transition={{ duration: 4, repeat: Infinity }}
|
||||
>
|
||||
<rect x="20" y="55" width="35" height="35" rx="2" className="fill-slate-300" />
|
||||
<rect x="65" y="55" width="35" height="35" rx="2" className="fill-slate-300" />
|
||||
</motion.g>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ExperienceIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Timeline line */}
|
||||
<motion.path
|
||||
d="M 20 100 H 100"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay }}
|
||||
/>
|
||||
|
||||
{/* Experience nodes */}
|
||||
{[
|
||||
{ x: 30, y: 80, label: "Agency" },
|
||||
{ x: 50, y: 60, label: "Corp" },
|
||||
{ x: 70, y: 40, label: "Startup" },
|
||||
{ x: 90, y: 20, label: "Now" }
|
||||
].map((node, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<motion.circle
|
||||
cx={node.x} cy={node.y} r="4"
|
||||
className={i === 3 ? "fill-slate-900" : "fill-slate-300"}
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.5 + i * 0.2, type: "spring" }}
|
||||
/>
|
||||
{i > 0 && (
|
||||
<motion.path
|
||||
d={`M ${30 + (i-1)*20} ${80 - (i-1)*20} L ${node.x} ${node.y}`}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 0.5 + (i-1) * 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{/* 15+ Years indicator */}
|
||||
<motion.text
|
||||
x="20" y="115"
|
||||
className="text-[8px] font-bold fill-slate-400 uppercase tracking-widest"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1.5 }}
|
||||
>
|
||||
15+ YEARS
|
||||
</motion.text>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const HeroArchitecture: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<motion.rect x="170" y="120" width="60" height="60" rx="8" className="stroke-slate-900 fill-white" strokeWidth="2"
|
||||
animate={{ scale: [1, 1.05, 1] }} transition={{ duration: 4, repeat: Infinity }} />
|
||||
{[
|
||||
{ x: 80, y: 60 }, { x: 320, y: 60 },
|
||||
{ x: 80, y: 240 }, { x: 320, y: 240 }
|
||||
].map((node, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<motion.path
|
||||
d={`M 200 150 L ${node.x} ${node.y}`}
|
||||
stroke="currentColor" strokeWidth="1" className="text-slate-400"
|
||||
animate={{ strokeDashoffset: [0, -10] }} strokeDasharray="4 4"
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx={node.x} cy={node.y} r="12"
|
||||
className="fill-white stroke-slate-300"
|
||||
strokeWidth="1"
|
||||
animate={{ scale: [1, 1.1, 1] }} transition={{ duration: 3, repeat: Infinity, delay: i * 0.5 }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const HeroMainIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 800 700" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Matrix-style Binary Rain Background */}
|
||||
<g className="opacity-[0.08]">
|
||||
{Array.from({ length: 20 }).map((_, col) => {
|
||||
const colX = 20 + col * 40;
|
||||
const speed = 8 + (col % 6);
|
||||
const startDelay = (col % 5);
|
||||
return (
|
||||
<motion.g
|
||||
key={`rain-col-${col}`}
|
||||
initial={{ y: -700 }}
|
||||
animate={{ y: 700 }}
|
||||
transition={{
|
||||
duration: speed,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
delay: startDelay,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 25 }).map((_, row) => (
|
||||
<text
|
||||
key={`${col}-${row}`}
|
||||
x={colX}
|
||||
y={row * 28}
|
||||
className="fill-slate-900 font-mono"
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
{(col + row) % 2 === 0 ? '1' : '0'}
|
||||
</text>
|
||||
))}
|
||||
</motion.g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Layer 1: Base Platform */}
|
||||
<motion.g
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<rect x="150" y="500" width="500" height="30" rx="4" className="fill-slate-100 stroke-slate-300" strokeWidth="1" />
|
||||
<rect x="170" y="510" width="460" height="10" rx="2" className="fill-slate-200" />
|
||||
{/* Binary on base */}
|
||||
<text x="180" y="518" className="fill-slate-400 font-mono" style={{ fontSize: 8 }}>01010101010101010101010101010101010101</text>
|
||||
</motion.g>
|
||||
|
||||
{/* Layer 2: Server/Database Layer */}
|
||||
<motion.g
|
||||
animate={{ y: [0, 6, 0] }}
|
||||
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut", delay: 0.3 }}
|
||||
>
|
||||
{/* Left Server Block */}
|
||||
<g transform="translate(200, 400)">
|
||||
<rect x="0" y="0" width="120" height="80" rx="6" className="fill-white stroke-slate-900" strokeWidth="2" />
|
||||
<rect x="10" y="10" width="100" height="15" rx="2" className="fill-slate-100" />
|
||||
<rect x="10" y="30" width="80" height="10" rx="2" className="fill-slate-200" />
|
||||
<rect x="10" y="45" width="60" height="10" rx="2" className="fill-slate-200" />
|
||||
<circle cx="100" cy="65" r="5" className="fill-slate-900" />
|
||||
<text x="15" y="20" className="fill-slate-500 font-mono" style={{ fontSize: 8 }}>SERVER</text>
|
||||
</g>
|
||||
|
||||
{/* Right Database Block */}
|
||||
<g transform="translate(480, 400)">
|
||||
<rect x="0" y="0" width="120" height="80" rx="6" className="fill-white stroke-slate-900" strokeWidth="2" />
|
||||
<rect x="10" y="10" width="100" height="15" rx="2" className="fill-slate-100" />
|
||||
<rect x="10" y="30" width="100" height="8" rx="2" className="fill-slate-200" />
|
||||
<rect x="10" y="42" width="100" height="8" rx="2" className="fill-slate-200" />
|
||||
<rect x="10" y="54" width="100" height="8" rx="2" className="fill-slate-200" />
|
||||
<text x="15" y="20" className="fill-slate-500 font-mono" style={{ fontSize: 8 }}>DATABASE</text>
|
||||
</g>
|
||||
|
||||
{/* Connection Lines */}
|
||||
<motion.path
|
||||
d="M 320 440 L 400 440 L 480 440"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-slate-400"
|
||||
strokeDasharray="6 4"
|
||||
animate={{ strokeDashoffset: [0, -20] }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
</motion.g>
|
||||
|
||||
{/* Layer 3: Browser/Website */}
|
||||
<motion.g
|
||||
animate={{ y: [0, 4, 0] }}
|
||||
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut", delay: 0.6 }}
|
||||
>
|
||||
{/* Browser Window */}
|
||||
<rect x="180" y="100" width="440" height="280" rx="8" className="fill-white stroke-slate-900" strokeWidth="2" />
|
||||
|
||||
{/* Browser Chrome */}
|
||||
<rect x="180" y="100" width="440" height="30" rx="8" className="fill-slate-900" />
|
||||
<rect x="180" y="120" width="440" height="10" className="fill-slate-900" />
|
||||
|
||||
{/* Browser Dots */}
|
||||
<circle cx="200" cy="115" r="5" className="fill-slate-600" />
|
||||
<circle cx="218" cy="115" r="5" className="fill-slate-600" />
|
||||
<circle cx="236" cy="115" r="5" className="fill-slate-600" />
|
||||
|
||||
{/* Address Bar */}
|
||||
<rect x="260" y="108" width="200" height="14" rx="3" className="fill-slate-700" />
|
||||
|
||||
{/* Website Content */}
|
||||
<g transform="translate(200, 150)">
|
||||
{/* Navigation */}
|
||||
<rect x="0" y="0" width="400" height="20" className="fill-slate-50" />
|
||||
<rect x="10" y="5" width="60" height="10" rx="2" className="fill-slate-900" />
|
||||
<rect x="280" y="5" width="30" height="10" rx="2" className="fill-slate-300" />
|
||||
<rect x="320" y="5" width="30" height="10" rx="2" className="fill-slate-300" />
|
||||
<rect x="360" y="5" width="30" height="10" rx="2" className="fill-slate-300" />
|
||||
|
||||
{/* Hero Section */}
|
||||
<rect x="0" y="30" width="400" height="100" className="fill-slate-100" />
|
||||
<rect x="20" y="50" width="180" height="16" rx="2" className="fill-slate-900" />
|
||||
<rect x="20" y="72" width="140" height="10" rx="2" className="fill-slate-400" />
|
||||
<rect x="20" y="88" width="100" height="10" rx="2" className="fill-slate-400" />
|
||||
<rect x="20" y="108" width="80" height="16" rx="4" className="fill-slate-900" />
|
||||
|
||||
{/* Hero Image Placeholder */}
|
||||
<rect x="240" y="40" width="140" height="80" rx="4" className="fill-slate-200" />
|
||||
<path d="M 280 80 L 310 60 L 340 80 L 310 100 Z" className="fill-slate-300" />
|
||||
|
||||
{/* Cards Section */}
|
||||
<g transform="translate(0, 140)">
|
||||
<rect x="0" y="0" width="125" height="70" rx="4" className="fill-slate-50 stroke-slate-200" strokeWidth="1" />
|
||||
<rect x="10" y="10" width="105" height="30" rx="2" className="fill-slate-200" />
|
||||
<rect x="10" y="48" width="80" height="8" rx="2" className="fill-slate-300" />
|
||||
|
||||
<rect x="137" y="0" width="125" height="70" rx="4" className="fill-slate-50 stroke-slate-200" strokeWidth="1" />
|
||||
<rect x="147" y="10" width="105" height="30" rx="2" className="fill-slate-200" />
|
||||
<rect x="147" y="48" width="80" height="8" rx="2" className="fill-slate-300" />
|
||||
|
||||
<rect x="274" y="0" width="125" height="70" rx="4" className="fill-slate-50 stroke-slate-200" strokeWidth="1" />
|
||||
<rect x="284" y="10" width="105" height="30" rx="2" className="fill-slate-200" />
|
||||
<rect x="284" y="48" width="80" height="8" rx="2" className="fill-slate-300" />
|
||||
</g>
|
||||
</g>
|
||||
</motion.g>
|
||||
|
||||
{/* Connecting Lines from Browser to Infrastructure */}
|
||||
<motion.g>
|
||||
<motion.path
|
||||
d="M 400 380 L 400 400"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-slate-400"
|
||||
strokeDasharray="4 4"
|
||||
animate={{ strokeDashoffset: [0, -16] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 260 480 L 260 500"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-slate-400"
|
||||
strokeDasharray="4 4"
|
||||
animate={{ strokeDashoffset: [0, -16] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M 540 480 L 540 500"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-slate-400"
|
||||
strokeDasharray="4 4"
|
||||
animate={{ strokeDashoffset: [0, -16] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
</motion.g>
|
||||
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ResponsibilityIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Shield / Responsibility shape */}
|
||||
<motion.path
|
||||
d="M 60 20 L 90 35 V 65 C 90 85 60 100 60 100 C 60 100 30 85 30 65 V 35 L 60 20 Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0, fill: "rgba(15, 23, 42, 0)" }}
|
||||
whileInView={{ pathLength: 1, fill: "rgba(15, 23, 42, 0.05)" }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.5, delay }}
|
||||
/>
|
||||
|
||||
{/* Core point */}
|
||||
<motion.circle
|
||||
cx="60" cy="55" r="8"
|
||||
className="fill-slate-900"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1, type: "spring" }}
|
||||
/>
|
||||
|
||||
{/* Responsibility lines */}
|
||||
{[0, 120, 240].map((angle, i) => {
|
||||
const x1 = 60 + Math.cos((angle * Math.PI) / 180) * 12;
|
||||
const y1 = 55 + Math.sin((angle * Math.PI) / 180) * 12;
|
||||
const x2 = 60 + Math.cos((angle * Math.PI) / 180) * 30;
|
||||
const y2 = 55 + Math.sin((angle * Math.PI) / 180) * 30;
|
||||
return (
|
||||
<motion.line
|
||||
key={i}
|
||||
x1={x1} y1={y1} x2={x2} y2={y2}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-400"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1.2 + i * 0.2 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const ResultIllustration: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Result Box */}
|
||||
<motion.rect
|
||||
x="30" y="30" width="60" height="60" rx="4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0, rotate: -10, opacity: 0 }}
|
||||
whileInView={{ pathLength: 1, rotate: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay }}
|
||||
/>
|
||||
|
||||
{/* Checkmark */}
|
||||
<motion.path
|
||||
d="M 45 60 L 55 70 L 75 50"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 1 }}
|
||||
/>
|
||||
|
||||
{/* Sparkles */}
|
||||
{[
|
||||
{ x: 25, y: 25 },
|
||||
{ x: 95, y: 35 },
|
||||
{ x: 85, y: 95 }
|
||||
].map((pos, i) => (
|
||||
<motion.path
|
||||
key={i}
|
||||
d={`M ${pos.x} ${pos.y-4} V ${pos.y+4} M ${pos.x-4} ${pos.y} H ${pos.x+4}`}
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-slate-300"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
whileInView={{ scale: 1, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1.5 + i * 0.2, type: "spring" }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,333 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IllustrationProps } from './types';
|
||||
|
||||
export const SystemArchitecture: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Central Core */}
|
||||
<motion.rect
|
||||
x="70" y="70" width="60" height="60" rx="12"
|
||||
className="fill-slate-900"
|
||||
initial={{ scale: 0, rotate: -45 }}
|
||||
whileInView={{ scale: 1, rotate: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ type: "spring", stiffness: 100, delay }}
|
||||
/>
|
||||
|
||||
{/* Orbiting Elements */}
|
||||
{[0, 90, 180, 270].map((angle, i) => {
|
||||
const rad = (angle * Math.PI) / 180;
|
||||
const x = 100 + Math.cos(rad) * 60 - 15;
|
||||
const y = 100 + Math.sin(rad) * 60 - 15;
|
||||
return (
|
||||
<motion.rect
|
||||
key={i}
|
||||
x={x} y={y} width="30" height="30" rx="8"
|
||||
className="fill-white stroke-slate-200"
|
||||
strokeWidth="1"
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
animate={{
|
||||
y: [y, y - 5, y],
|
||||
}}
|
||||
transition={{
|
||||
delay: delay + 0.2 + i * 0.1,
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatDelay: i * 0.5,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Connection Lines */}
|
||||
<motion.path
|
||||
d="M 100 40 V 70 M 100 130 V 160 M 40 100 H 70 M 130 100 H 160"
|
||||
stroke="currentColor" strokeWidth="1"
|
||||
className="text-slate-200"
|
||||
strokeDasharray="4 4"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay: delay + 0.6 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SpeedPerformance: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* The "Built" Foundation */}
|
||||
<motion.path
|
||||
d="M 40 160 H 160"
|
||||
stroke="currentColor" strokeWidth="4" strokeLinecap="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay }}
|
||||
/>
|
||||
|
||||
{/* Bricks forming a structure */}
|
||||
{[
|
||||
{ x: 50, y: 130, w: 30 },
|
||||
{ x: 85, y: 130, w: 30 },
|
||||
{ x: 120, y: 130, w: 30 },
|
||||
{ x: 65, y: 105, w: 30 },
|
||||
{ x: 105, y: 105, w: 30 },
|
||||
].map((brick, i) => (
|
||||
<motion.rect
|
||||
key={i}
|
||||
x={brick.x} y={brick.y} width={brick.w} height="20" rx="2"
|
||||
className="fill-slate-100 stroke-slate-200"
|
||||
strokeWidth="1"
|
||||
initial={{ opacity: 0, y: brick.y + 10 }}
|
||||
whileInView={{ opacity: 1, y: brick.y }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.2 + i * 0.1 }}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Lightning Bolt emerging from the "Built" structure */}
|
||||
<motion.path
|
||||
d="M 110 30 L 80 80 H 100 L 70 130"
|
||||
stroke="currentColor" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0, filter: "drop-shadow(0 0 0px rgba(0,0,0,0))" }}
|
||||
whileInView={{ pathLength: 1, filter: "drop-shadow(0 0 8px rgba(0,0,0,0.1))" }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 1, ease: "easeOut" }}
|
||||
/>
|
||||
|
||||
{/* Speed Lines */}
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.path
|
||||
key={i}
|
||||
d={`M ${130 + i * 10} ${50 + i * 20} H ${150 + i * 10}`}
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 10, opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay: delay + 1.2 + i * 0.2 }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SolidFoundation: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Stacked Blocks */}
|
||||
<motion.rect
|
||||
x="40" y="140" width="120" height="30" rx="4"
|
||||
className="fill-slate-900"
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay }}
|
||||
/>
|
||||
<motion.rect
|
||||
x="60" y="100" width="80" height="30" rx="4"
|
||||
className="fill-slate-400"
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.2 }}
|
||||
/>
|
||||
<motion.rect
|
||||
x="80" y="60" width="40" height="30" rx="4"
|
||||
className="fill-slate-200"
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.4 }}
|
||||
/>
|
||||
|
||||
{/* Shield Icon Overlay */}
|
||||
<motion.path
|
||||
d="M 100 30 L 120 40 V 60 C 120 80 100 90 100 90 C 100 90 80 80 80 60 V 40 L 100 30 Z"
|
||||
className="fill-white stroke-slate-900"
|
||||
strokeWidth="2"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ type: "spring", delay: delay + 0.8 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LayerSeparation: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Top Layer (Content) */}
|
||||
<motion.g
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay }}
|
||||
>
|
||||
<rect x="40" y="40" width="120" height="40" rx="8" className="fill-white stroke-slate-200" strokeWidth="1" />
|
||||
<rect x="55" y="55" width="30" height="10" rx="2" className="fill-slate-100" />
|
||||
<rect x="95" y="55" width="50" height="10" rx="2" className="fill-slate-100" />
|
||||
</motion.g>
|
||||
|
||||
{/* Gap with Arrows */}
|
||||
<motion.path
|
||||
d="M 100 90 V 110"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
strokeDasharray="4 4"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.4 }}
|
||||
/>
|
||||
|
||||
{/* Bottom Layer (Code/Logic) */}
|
||||
<motion.g
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.2 }}
|
||||
>
|
||||
<rect x="40" y="120" width="120" height="40" rx="8" className="fill-slate-900" />
|
||||
<path d="M 60 140 L 70 135 L 60 130 M 140 140 L 130 135 L 140 130" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</motion.g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DirectService: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Two Nodes */}
|
||||
<motion.circle
|
||||
cx="50" cy="100" r="15"
|
||||
className="fill-slate-200"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="150" cy="100" r="15"
|
||||
className="fill-slate-900"
|
||||
initial={{ scale: 0 }}
|
||||
whileInView={{ scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 0.2 }}
|
||||
/>
|
||||
|
||||
{/* Direct Connection */}
|
||||
<motion.path
|
||||
d="M 65 100 H 135"
|
||||
stroke="currentColor" strokeWidth="3" strokeLinecap="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: delay + 0.4 }}
|
||||
/>
|
||||
|
||||
{/* Pulse moving between */}
|
||||
<motion.circle r="4" className="fill-white">
|
||||
<animateMotion
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
path="M 65 100 H 135"
|
||||
/>
|
||||
</motion.circle>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AgencyChaos: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Messy Path */}
|
||||
<motion.path
|
||||
d="M 30 100 C 50 40, 70 160, 90 100 C 110 40, 130 160, 170 100"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 2, delay }}
|
||||
/>
|
||||
|
||||
{/* Intersecting Circles (Meetings) */}
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.circle
|
||||
key={i}
|
||||
cx={60 + i * 40} cy={100 + (i % 2 === 0 ? -30 : 30)} r="20"
|
||||
className="stroke-slate-100 fill-white"
|
||||
strokeWidth="1"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
animate={{ scale: [1, 1.1, 1] }}
|
||||
transition={{
|
||||
delay: delay + 0.5 + i * 0.3,
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
repeatDelay: i * 0.5
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Slate "X" or Stop signs - NO COLOR */}
|
||||
<motion.path
|
||||
d="M 160 90 L 180 110 M 180 90 L 160 110"
|
||||
stroke="currentColor" strokeWidth="3" strokeLinecap="round"
|
||||
className="text-slate-300"
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
whileInView={{ opacity: 0.5, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1.5 }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TaskDone: React.FC<IllustrationProps> = ({ className = "", delay = 0 }) => (
|
||||
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Checkmark Circle */}
|
||||
<motion.circle
|
||||
cx="100" cy="100" r="60"
|
||||
className="stroke-slate-900"
|
||||
strokeWidth="2"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, delay }}
|
||||
/>
|
||||
|
||||
{/* Checkmark */}
|
||||
<motion.path
|
||||
d="M 70 100 L 90 120 L 135 75"
|
||||
stroke="currentColor" strokeWidth="8" strokeLinecap="round" strokeLinejoin="round"
|
||||
className="text-slate-900"
|
||||
initial={{ pathLength: 0 }}
|
||||
whileInView={{ pathLength: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: delay + 0.8 }}
|
||||
/>
|
||||
|
||||
{/* Confetti/Sparkles */}
|
||||
{[0, 45, 90, 135, 180, 225, 270, 315].map((angle, i) => {
|
||||
const rad = (angle * Math.PI) / 180;
|
||||
const x1 = 100 + Math.cos(rad) * 70;
|
||||
const y1 = 100 + Math.sin(rad) * 70;
|
||||
const x2 = 100 + Math.cos(rad) * 85;
|
||||
const y2 = 100 + Math.sin(rad) * 85;
|
||||
return (
|
||||
<motion.line
|
||||
key={i}
|
||||
x1={x1} y1={y1} x2={x2} y2={y2}
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
||||
className="text-slate-200"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + 1.2 + i * 0.05 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
16
apps/web/src/components/Landing/Illustrations/index.ts
Normal file
16
apps/web/src/components/Landing/Illustrations/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from './types';
|
||||
export * from './ConceptCommunication';
|
||||
export * from './ConceptPrototyping';
|
||||
export * from './ConceptCode';
|
||||
export * from './ConceptPrice';
|
||||
export * from './ConceptWebsite';
|
||||
export * from './ConceptSystem';
|
||||
export * from './ConceptAutomation';
|
||||
export * from './ConceptTarget';
|
||||
export * from './ConceptMessy';
|
||||
export * from './HeroArchitecture';
|
||||
export * from './HeroMainIllustration';
|
||||
export * from './ExperienceIllustration';
|
||||
export * from './ResponsibilityIllustration';
|
||||
export * from './ResultIllustration';
|
||||
export * from './WebsitesDescriptive';
|
||||
8
apps/web/src/components/Landing/Illustrations/types.ts
Normal file
8
apps/web/src/components/Landing/Illustrations/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IllustrationProps {
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
259
apps/web/src/components/Landing/ParticleNetwork.tsx
Normal file
259
apps/web/src/components/Landing/ParticleNetwork.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
baseX: number; // The "home" x position (in the gutters)
|
||||
baseY: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
size: number;
|
||||
alpha: number;
|
||||
targetAlpha: number;
|
||||
}
|
||||
|
||||
interface ParticleNetworkProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ParticleNetwork: React.FC<ParticleNetworkProps> = ({ className = '' }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const particlesRef = useRef<Particle[]>([]);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const scrollRef = useRef<number>(0);
|
||||
const mouseRef = useRef<{ x: number; y: number }>({ x: -1000, y: -1000 });
|
||||
const dimensionsRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
particleCount: 60, // Reduced count for cleaner look
|
||||
connectionDistance: 180,
|
||||
mouseRadius: 250,
|
||||
baseSpeed: 0.2,
|
||||
gutterWidth: 0.25, // 25% of width on each side
|
||||
colors: {
|
||||
particle: 'rgba(148, 163, 184, 0.8)', // slate-400
|
||||
line: 'rgba(203, 213, 225, 0.4)', // slate-300
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize particles
|
||||
const initParticles = useCallback((width: number, height: number) => {
|
||||
const particles: Particle[] = [];
|
||||
const count = config.particleCount;
|
||||
|
||||
// Create particles primarily in the side gutters
|
||||
for (let i = 0; i < count; i++) {
|
||||
const isLeft = Math.random() > 0.5;
|
||||
// Random position within the gutter (0-25% or 75-100%)
|
||||
const gutterX = isLeft
|
||||
? Math.random() * (width * config.gutterWidth)
|
||||
: width - (Math.random() * (width * config.gutterWidth));
|
||||
|
||||
// Add some occasional strays near the content but not IN it
|
||||
const x = gutterX;
|
||||
const y = Math.random() * height;
|
||||
|
||||
particles.push({
|
||||
x,
|
||||
y,
|
||||
baseX: x,
|
||||
baseY: y,
|
||||
vx: (Math.random() - 0.5) * 0.1, // Very slight horizontal drift
|
||||
vy: 0.2 + Math.random() * 0.3, // Consistent downward flow
|
||||
size: Math.random() * 1.5 + 0.5,
|
||||
alpha: 0,
|
||||
targetAlpha: Math.random() * 0.6 + 0.2,
|
||||
});
|
||||
}
|
||||
|
||||
particlesRef.current = particles;
|
||||
}, [config.particleCount, config.gutterWidth]);
|
||||
|
||||
// Animation loop
|
||||
const animate = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
const { width, height } = dimensionsRef.current;
|
||||
const scroll = scrollRef.current;
|
||||
const mouse = mouseRef.current;
|
||||
const particles = particlesRef.current;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Update and draw particles
|
||||
particles.forEach((p, i) => {
|
||||
// 1. Movement
|
||||
// Apply downward flow
|
||||
p.y += p.vy;
|
||||
p.x += p.vx;
|
||||
|
||||
// Wrap around vertical
|
||||
if (p.y > height) {
|
||||
p.y = -10;
|
||||
p.x = p.baseX; // Reset X to base to keep structure
|
||||
}
|
||||
|
||||
// 2. Mouse Interaction (Repulsion)
|
||||
const dx = p.x - mouse.x;
|
||||
const dy = p.y - mouse.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < config.mouseRadius) {
|
||||
const force = (config.mouseRadius - dist) / config.mouseRadius;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const pushX = Math.cos(angle) * force * 2;
|
||||
const pushY = Math.sin(angle) * force * 2;
|
||||
|
||||
p.x += pushX;
|
||||
p.y += pushY;
|
||||
}
|
||||
|
||||
// 3. Return to "Lane" (Elasticity)
|
||||
// Gently pull x back towards baseX if not influenced by mouse
|
||||
if (dist >= config.mouseRadius) {
|
||||
p.x += (p.baseX - p.x) * 0.02;
|
||||
}
|
||||
|
||||
// 4. Scroll Effect (Parallax/Speed)
|
||||
// Scroll adds a temporary velocity boost or shift
|
||||
// We use a simple factor here, but could be more complex
|
||||
const scrollFactor = Math.max(0, Math.min(1, scroll * 0.001));
|
||||
p.y += scrollFactor * 0.5; // Move faster when scrolled down?
|
||||
// Actually, let's make them react to scroll speed if we tracked delta,
|
||||
// but for now just position shift based on scroll is handled by the container being fixed.
|
||||
// Let's add a subtle wave based on scroll position
|
||||
p.x += Math.sin(scroll * 0.002 + p.y * 0.01) * 0.2;
|
||||
|
||||
// Fade in
|
||||
if (p.alpha < p.targetAlpha) p.alpha += 0.01;
|
||||
|
||||
// Draw Particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(148, 163, 184, ${p.alpha})`;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Draw Connections
|
||||
// We only connect particles that are close enough
|
||||
ctx.lineWidth = 0.5;
|
||||
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
const p1 = particles[i];
|
||||
// Optimization: only check particles after this one
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const p2 = particles[j];
|
||||
|
||||
// Quick check for distance
|
||||
const dx = p1.x - p2.x;
|
||||
const dy = p1.y - p2.y;
|
||||
|
||||
// Optimization: skip sqrt if obviously too far
|
||||
if (Math.abs(dx) > config.connectionDistance || Math.abs(dy) > config.connectionDistance) continue;
|
||||
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < config.connectionDistance) {
|
||||
// Calculate opacity based on distance
|
||||
const opacity = (1 - dist / config.connectionDistance) * 0.3;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = `rgba(203, 213, 225, ${opacity})`;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
}, [config]);
|
||||
|
||||
// Handle resize
|
||||
const handleResize = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!container || !canvas) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
dimensionsRef.current = { width: rect.width, height: rect.height };
|
||||
initParticles(rect.width, rect.height);
|
||||
}, [initParticles]);
|
||||
|
||||
// Handle scroll
|
||||
const handleScroll = useCallback(() => {
|
||||
scrollRef.current = window.scrollY;
|
||||
}, []);
|
||||
|
||||
// Handle mouse move
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
mouseRef.current = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top, // Fixed position, so no scrollY needed for mouse relative to canvas
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle mouse leave
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
mouseRef.current = { x: -1000, y: -1000 };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
handleScroll();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
window.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseleave', handleMouseLeave);
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
};
|
||||
}, [handleResize, handleScroll, handleMouseMove, handleMouseLeave, animate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`fixed inset-0 pointer-events-none -z-10 ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticleNetwork;
|
||||
50
apps/web/src/components/Landing/ServiceCard.tsx
Normal file
50
apps/web/src/components/Landing/ServiceCard.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Reveal } from '../Reveal';
|
||||
|
||||
interface ServiceCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
linkHref?: string;
|
||||
linkLabel?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const ServiceCard: React.FC<ServiceCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
linkHref,
|
||||
linkLabel,
|
||||
delay = 0,
|
||||
}) => {
|
||||
return (
|
||||
<Reveal delay={delay}>
|
||||
<div className="group relative p-10 md:p-12 border border-slate-100 rounded-3xl hover:border-slate-900 transition-all duration-700 bg-white hover:shadow-2xl hover:shadow-slate-100/50 overflow-hidden">
|
||||
{/* Decorative Corner */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 border-t border-r border-slate-100 rounded-tr-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
|
||||
<div className="absolute top-8 right-8 w-2 h-2 bg-slate-900 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-700 delay-100"></div>
|
||||
|
||||
{/* Decorative background text */}
|
||||
<div className="absolute -right-4 -bottom-4 text-8xl md:text-9xl font-bold text-slate-50 select-none group-hover:text-slate-100 transition-colors duration-700 -z-10">
|
||||
{title.charAt(0)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-4xl md:text-6xl font-bold text-slate-900 mb-6 tracking-tighter group-hover:tracking-tight transition-all duration-700">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-xl md:text-2xl text-slate-500 font-serif italic mb-10 leading-tight max-w-3xl group-hover:text-slate-700 transition-colors duration-700">
|
||||
{description}
|
||||
</p>
|
||||
{linkHref && linkLabel && (
|
||||
<Link
|
||||
href={linkHref}
|
||||
className="inline-flex items-center gap-4 text-slate-900 font-bold text-[10px] uppercase tracking-[0.2em] group/link"
|
||||
>
|
||||
{linkLabel}
|
||||
<div className="w-8 h-px bg-slate-200 group-hover/link:bg-slate-900 group-hover/link:w-16 transition-all duration-500"></div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
};
|
||||
10
apps/web/src/components/Landing/index.ts
Normal file
10
apps/web/src/components/Landing/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './AbstractLines';
|
||||
export * from './ExplanatoryIllustrations';
|
||||
export * from './ConceptIllustrations';
|
||||
export * from './Illustrations';
|
||||
export * from './ComparisonRow';
|
||||
export * from './FeatureCard';
|
||||
export * from './HeroItem';
|
||||
export * from './ParticleNetwork';
|
||||
export * from './ServiceCard';
|
||||
|
||||
Reference in New Issue
Block a user