v3
This commit is contained in:
@@ -0,0 +1,844 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Globe,
|
||||
FileText,
|
||||
Code,
|
||||
Shield,
|
||||
// Search, // Not used in current implementation
|
||||
Zap,
|
||||
Database,
|
||||
// Lock, // Not used in current implementation
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Bot,
|
||||
Sparkles,
|
||||
FileCode,
|
||||
Network,
|
||||
Info,
|
||||
Eye
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ScoreChart from "./ScoreChart";
|
||||
import RadarChart from "./RadarChart";
|
||||
import MetricBars from "./MetricBars";
|
||||
|
||||
interface ControlPanelProps {
|
||||
isAnalyzing: boolean;
|
||||
showResults: boolean;
|
||||
url: string;
|
||||
analysisData?: any;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
interface CheckItem {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: any;
|
||||
status: 'pending' | 'checking' | 'pass' | 'fail' | 'warning';
|
||||
score?: number;
|
||||
details?: string;
|
||||
recommendation?: string;
|
||||
actionItems?: string[];
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export default function ControlPanel({
|
||||
isAnalyzing,
|
||||
showResults,
|
||||
url,
|
||||
analysisData,
|
||||
onReset,
|
||||
}: ControlPanelProps) {
|
||||
const [showAIAnalysis, setShowAIAnalysis] = useState(false); // Reserved for AI analysis feature
|
||||
const [aiInsights, setAiInsights] = useState<CheckItem[]>([]);
|
||||
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
|
||||
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
|
||||
const [checks, setChecks] = useState<CheckItem[]>([
|
||||
{
|
||||
id: 'heading-structure',
|
||||
label: 'Heading Hierarchy',
|
||||
description: 'H1-H6 structure',
|
||||
icon: FileText,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'readability',
|
||||
label: 'Readability',
|
||||
description: 'Content clarity',
|
||||
icon: Globe,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'meta-tags',
|
||||
label: 'Metadata Quality',
|
||||
description: 'Title, desc, author',
|
||||
icon: FileCode,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'semantic-html',
|
||||
label: 'Semantic HTML',
|
||||
description: 'Proper HTML5 tags',
|
||||
icon: Code,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'accessibility',
|
||||
label: 'Accessibility',
|
||||
description: 'Alt text & ARIA',
|
||||
icon: Eye,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'llms-txt',
|
||||
label: 'LLMs.txt',
|
||||
description: 'AI permissions',
|
||||
icon: Bot,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'robots-txt',
|
||||
label: 'Robots.txt',
|
||||
description: 'Crawler rules',
|
||||
icon: Shield,
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'sitemap',
|
||||
label: 'Sitemap',
|
||||
description: 'Site structure',
|
||||
icon: Network,
|
||||
status: 'pending',
|
||||
},
|
||||
]);
|
||||
|
||||
const [overallScore, setOverallScore] = useState(0);
|
||||
const [currentCheckIndex, setCurrentCheckIndex] = useState(-1);
|
||||
const [selectedCheck, setSelectedCheck] = useState<string | null>(null);
|
||||
const [hoveredCheck, setHoveredCheck] = useState<string | null>(null);
|
||||
const [enhancedScore, setEnhancedScore] = useState(0);
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'chart' | 'bars'>('grid');
|
||||
|
||||
useEffect(() => {
|
||||
if (analysisData && analysisData.checks && showResults) {
|
||||
// Use real data from API
|
||||
const mappedChecks = analysisData.checks.map((check: any) => ({
|
||||
...check,
|
||||
icon: checks.find(c => c.id === check.id)?.icon || FileText,
|
||||
description: check.details || checks.find(c => c.id === check.id)?.description,
|
||||
}));
|
||||
setChecks(mappedChecks);
|
||||
setCombinedChecks(mappedChecks); // Initialize with basic checks
|
||||
setOverallScore(analysisData.overallScore || 0);
|
||||
setCurrentCheckIndex(-1);
|
||||
|
||||
// If AI analysis should auto-start, handle the promise
|
||||
if (analysisData.autoStartAI && analysisData.aiAnalysisPromise) {
|
||||
console.log('Auto-starting AI analysis with promise');
|
||||
setIsAnalyzingAI(true);
|
||||
setShowAIAnalysis(true);
|
||||
|
||||
// Add placeholder AI tiles immediately with actual titles
|
||||
const placeholderAIChecks = [
|
||||
{
|
||||
id: 'ai-loading-0',
|
||||
label: 'Content Quality for AI',
|
||||
description: 'Analyzing content signal ratio...',
|
||||
icon: Sparkles,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-1',
|
||||
label: 'Information Architecture',
|
||||
description: 'Evaluating page structure...',
|
||||
icon: Bot,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-2',
|
||||
label: 'Crawlability Patterns',
|
||||
description: 'Checking JavaScript usage...',
|
||||
icon: Database,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-3',
|
||||
label: 'AI Training Value',
|
||||
description: 'Assessing training potential...',
|
||||
icon: Network,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-4',
|
||||
label: 'Knowledge Extraction',
|
||||
description: 'Analyzing entity definitions...',
|
||||
icon: FileCode,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-5',
|
||||
label: 'Template Quality',
|
||||
description: 'Reviewing semantic structure...',
|
||||
icon: Shield,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-6',
|
||||
label: 'Content Depth',
|
||||
description: 'Measuring content richness...',
|
||||
icon: Zap,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-7',
|
||||
label: 'Machine Readability',
|
||||
description: 'Testing extraction reliability...',
|
||||
icon: Globe,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
}
|
||||
];
|
||||
|
||||
// Add loading AI tiles with staggered animation
|
||||
placeholderAIChecks.forEach((check, idx) => {
|
||||
setTimeout(() => {
|
||||
setCombinedChecks(prev => [...prev, check]);
|
||||
}, 100 * (idx + 1));
|
||||
});
|
||||
|
||||
// Handle the AI analysis promise
|
||||
analysisData.aiAnalysisPromise
|
||||
.then(async (aiResponse: any) => {
|
||||
if (aiResponse) {
|
||||
const data = await aiResponse.json();
|
||||
if (data.success && data.insights) {
|
||||
// Convert AI insights to CheckItem format
|
||||
const aiChecks: CheckItem[] = data.insights.map((insight: any, idx: number) => ({
|
||||
...insight,
|
||||
icon: [Sparkles, Bot, Database, Network, FileCode, Shield, Zap, Globe][idx % 8],
|
||||
description: insight.details?.substring(0, 60) + '...' || 'AI Analysis',
|
||||
isAI: true,
|
||||
}));
|
||||
|
||||
setAiInsights(aiChecks);
|
||||
|
||||
// Replace loading tiles with real AI tiles
|
||||
setCombinedChecks(prev => {
|
||||
// Remove loading tiles
|
||||
const withoutLoading = prev.filter(c => !(c as any).isLoading);
|
||||
// Add real AI tiles
|
||||
return [...withoutLoading, ...aiChecks];
|
||||
});
|
||||
|
||||
// Calculate enhanced score
|
||||
if (data.insights.length > 0) {
|
||||
const aiScores = data.insights.map((i: any) => i.score || 0);
|
||||
const avgAiScore = aiScores.reduce((a: number, b: number) => a + b, 0) / aiScores.length;
|
||||
const combinedScore = Math.round((overallScore * 0.6) + (avgAiScore * 0.4));
|
||||
setEnhancedScore(combinedScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('AI analysis error:', error);
|
||||
// Remove loading tiles on error
|
||||
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsAnalyzingAI(false);
|
||||
});
|
||||
}
|
||||
} else if (isAnalyzing) {
|
||||
// Reset all checks when starting analysis
|
||||
const resetChecks = checks.map(check => ({ ...check, status: 'pending' as const }));
|
||||
setChecks(resetChecks);
|
||||
setCombinedChecks(resetChecks); // Reset combined checks too
|
||||
setCurrentCheckIndex(0);
|
||||
setOverallScore(0);
|
||||
|
||||
// Visual animation while waiting for real results
|
||||
const checkInterval = setInterval(() => {
|
||||
setCurrentCheckIndex(prev => {
|
||||
if (prev >= checks.length - 1) {
|
||||
clearInterval(checkInterval);
|
||||
return prev;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
return () => clearInterval(checkInterval);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isAnalyzing, showResults, analysisData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentCheckIndex >= 0 && currentCheckIndex < checks.length && isAnalyzing) {
|
||||
// Mark current as checking during animation
|
||||
setChecks(prev => prev.map((check, index) => {
|
||||
if (index === currentCheckIndex) {
|
||||
return { ...check, status: 'checking' };
|
||||
}
|
||||
if (index < currentCheckIndex) {
|
||||
return { ...check, status: 'checking' };
|
||||
}
|
||||
return check;
|
||||
}));
|
||||
|
||||
// Update combinedChecks to show the animation
|
||||
setCombinedChecks(prev => prev.map((check, index) => {
|
||||
if (index === currentCheckIndex) {
|
||||
return { ...check, status: 'checking' };
|
||||
}
|
||||
if (index < currentCheckIndex) {
|
||||
return { ...check, status: 'checking' };
|
||||
}
|
||||
return check;
|
||||
}));
|
||||
}
|
||||
}, [currentCheckIndex, checks.length, isAnalyzing]);
|
||||
|
||||
const getStatusIcon = (status: CheckItem['status']) => {
|
||||
switch (status) {
|
||||
case 'checking':
|
||||
return <Loader2 className="w-16 h-16 text-heat-100 animate-spin" />;
|
||||
case 'pass':
|
||||
return <CheckCircle2 className="w-16 h-16 text-accent-black" />;
|
||||
case 'fail':
|
||||
return <XCircle className="w-16 h-16 text-heat-200" />;
|
||||
case 'warning':
|
||||
return <AlertCircle className="w-16 h-16 text-heat-100" />;
|
||||
default:
|
||||
return <div className="w-16 h-16 rounded-full border border-black-alpha-8" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function available but not used in current render
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 80) return "text-accent-black";
|
||||
if (score >= 60) return "text-accent-black";
|
||||
return "text-accent-black";
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-full max-w-[1200px] mx-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="text-center mb-48 pt-24 md:pt-0"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<h2 className="text-title-h2 text-accent-black mb-12">AI Readiness Analysis</h2>
|
||||
<p className="text-body-large text-black-alpha-64">Single-page snapshot of {url}</p>
|
||||
|
||||
{showResults && (
|
||||
<>
|
||||
{/* View Mode Toggle - Moved above score */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mt-24 mb-20 flex justify-center gap-4"
|
||||
>
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
|
||||
viewMode === 'grid'
|
||||
? 'bg-accent-black text-white shadow-md'
|
||||
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
|
||||
}`}
|
||||
>
|
||||
Grid View
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('chart')}
|
||||
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
|
||||
viewMode === 'chart'
|
||||
? 'bg-accent-black text-white shadow-md'
|
||||
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
|
||||
}`}
|
||||
>
|
||||
Radar Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('bars')}
|
||||
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
|
||||
viewMode === 'bars'
|
||||
? 'bg-accent-black text-white shadow-md'
|
||||
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
|
||||
}`}
|
||||
>
|
||||
Bar Chart
|
||||
</button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", delay: 0.5 }}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<ScoreChart
|
||||
score={enhancedScore > 0 ? enhancedScore : overallScore}
|
||||
enhanced={enhancedScore > 0}
|
||||
size={180}
|
||||
/>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Conditional rendering based on view mode */}
|
||||
{viewMode === 'grid' && (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-12 mb-40 px-40 relative">
|
||||
{combinedChecks.map((check, index) => {
|
||||
const isActive = index === currentCheckIndex;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={check.id}
|
||||
initial={(check as any).isAI ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: isActive ? 1.05 : 1,
|
||||
}}
|
||||
transition={{
|
||||
delay: (check as any).isAI ? 0 : index * 0.1,
|
||||
scale: { type: "spring", stiffness: 300 }
|
||||
}}
|
||||
className={`
|
||||
relative p-16 rounded-8 transition-all bg-accent-white border
|
||||
${(check as any).isAI ? 'border-heat-100 border-opacity-40 bg-gradient-to-br from-accent-white to-heat-4' : 'border-black-alpha-8'}
|
||||
${isActive ? 'border-heat-100 shadow-lg' : ''}
|
||||
${check.status !== 'pending' && check.status !== 'checking' ? 'cursor-pointer hover:shadow-md' : ''}
|
||||
${(check as any).isLoading ? 'animate-pulse' : ''}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (check.status !== 'pending' && check.status !== 'checking') {
|
||||
setSelectedCheck(selectedCheck === check.id ? null : check.id);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHoveredCheck(check.id)}
|
||||
onMouseLeave={() => setHoveredCheck(null)}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="flex items-start justify-end mb-12">
|
||||
{getStatusIcon(check.status)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-label-large mb-4 text-accent-black font-medium flex items-center gap-6">
|
||||
{check.label}
|
||||
{check.tooltip && !aiInsights.some(ai => ai.id === check.id) && (
|
||||
<div className="relative inline-block">
|
||||
<Info className="w-14 h-14 text-black-alpha-32 hover:text-black-alpha-64 transition-colors" />
|
||||
<AnimatePresence>
|
||||
{hoveredCheck === check.id && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 5 }}
|
||||
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-8 w-200 p-8 bg-accent-black text-white text-body-x-small rounded-6 shadow-lg z-50 pointer-events-none"
|
||||
>
|
||||
{check.tooltip}
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-accent-black" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<p className="text-body-small text-black-alpha-64">
|
||||
{check.description}
|
||||
</p>
|
||||
|
||||
{check.status !== 'pending' && check.status !== 'checking' && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-8"
|
||||
>
|
||||
<div className="h-2 bg-black-alpha-4 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className={`
|
||||
h-full rounded-full
|
||||
${check.status === 'pass' ? 'bg-accent-black' : ''}
|
||||
${check.status === 'warning' ? 'bg-heat-100' : ''}
|
||||
${check.status === 'fail' ? 'bg-heat-200' : ''}
|
||||
`}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${check.score}%` }}
|
||||
transition={{ duration: 0.5 }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-label-x-small text-black-alpha-32 mt-4 text-center"
|
||||
>
|
||||
Click for details
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded Details */}
|
||||
<AnimatePresence>
|
||||
{selectedCheck === check.id && check.details && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="mt-12 pt-12 border-t border-black-alpha-8"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-48 mb-2">Status</div>
|
||||
<div className="text-body-small text-accent-black">{check.details}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-48 mb-2">Recommendation</div>
|
||||
<div className="text-body-small text-black-alpha-64">{check.recommendation}</div>
|
||||
{check.actionItems && check.actionItems.length > 0 && (
|
||||
<ul className="mt-4 space-y-2">
|
||||
{check.actionItems.map((item: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-6 text-body-small text-black-alpha-64">
|
||||
<span className="text-heat-100 mt-1">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Radar Chart View */}
|
||||
{viewMode === 'chart' && showResults && (
|
||||
<div>
|
||||
<motion.div
|
||||
className="flex justify-center gap-40 mb-40"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Basic Analysis Chart */}
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="text-label-large text-accent-black mb-16 font-medium">Basic Analysis</h3>
|
||||
<RadarChart
|
||||
data={checks
|
||||
.filter(check => check.status !== 'pending' && check.status !== 'checking')
|
||||
.slice(0, 8)
|
||||
.map(check => ({
|
||||
label: check.label.length > 12 ? check.label.substring(0, 12) + '...' : check.label,
|
||||
score: check.score || 0
|
||||
}))}
|
||||
size={350}
|
||||
/>
|
||||
<div className="mt-16 text-center">
|
||||
<div className="text-title-h3 text-accent-black">{overallScore}%</div>
|
||||
<div className="text-label-small text-black-alpha-48">Overall Score</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VS Indicator */}
|
||||
{aiInsights.length > 0 && (
|
||||
<motion.div
|
||||
className="flex items-center"
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2, type: "spring" }}
|
||||
>
|
||||
<div className="text-label-large text-black-alpha-32 font-medium">VS</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* AI Analysis Chart - Only show if AI insights exist */}
|
||||
{aiInsights.length > 0 && (
|
||||
<motion.div
|
||||
className="flex flex-col items-center"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<h3 className="text-label-large text-heat-100 mb-16 font-medium">AI Enhanced Analysis</h3>
|
||||
<RadarChart
|
||||
data={aiInsights
|
||||
.filter(check => check.status !== 'pending' && check.status !== 'checking')
|
||||
.slice(0, 8)
|
||||
.map(check => ({
|
||||
label: check.label.length > 12 ? check.label.substring(0, 12) + '...' : check.label,
|
||||
score: check.score || 0
|
||||
}))}
|
||||
size={350}
|
||||
/>
|
||||
<div className="mt-16 text-center">
|
||||
<div className="text-title-h3 text-heat-100">
|
||||
{Math.round(aiInsights.reduce((sum, check) => sum + (check.score || 0), 0) / aiInsights.length)}%
|
||||
</div>
|
||||
<div className="text-label-small text-heat-100 opacity-60">AI Score</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Comparison Summary */}
|
||||
{aiInsights.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<div className="inline-flex items-center gap-8 px-16 py-8 bg-heat-4 rounded-8">
|
||||
<span className="text-label-medium text-accent-black">
|
||||
AI analysis found {aiInsights.filter(i => i.score && i.score < 50).length} additional areas for improvement
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bar Chart View */}
|
||||
{viewMode === 'bars' && showResults && (
|
||||
<motion.div
|
||||
className="px-40 mb-40"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<MetricBars
|
||||
metrics={combinedChecks
|
||||
.filter(check => check.status !== 'pending' && check.status !== 'checking')
|
||||
.map(check => ({
|
||||
label: check.label,
|
||||
score: check.score || 0,
|
||||
status: check.status as 'pass' | 'warning' | 'fail',
|
||||
category: (check as any).isAI ? 'ai' :
|
||||
['robots-txt', 'sitemap', 'llms-txt'].includes(check.id) ? 'domain' : 'page',
|
||||
details: check.details,
|
||||
recommendation: check.recommendation,
|
||||
actionItems: check.actionItems
|
||||
}))}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
{showResults && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
className="flex gap-12 justify-center"
|
||||
>
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="px-20 py-10 bg-accent-white border border-black-alpha-8 hover:bg-black-alpha-4 rounded-8 text-label-medium transition-all"
|
||||
>
|
||||
Analyze Another Site
|
||||
</button>
|
||||
{true && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
setIsAnalyzingAI(true);
|
||||
setShowAIAnalysis(true);
|
||||
|
||||
// Add placeholder AI tiles immediately with actual titles
|
||||
const placeholderAIChecks = [
|
||||
{
|
||||
id: 'ai-loading-0',
|
||||
label: 'Content Quality for AI',
|
||||
description: 'Analyzing content signal ratio...',
|
||||
icon: Sparkles,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-1',
|
||||
label: 'Information Architecture',
|
||||
description: 'Evaluating page structure...',
|
||||
icon: Bot,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-2',
|
||||
label: 'Crawlability Patterns',
|
||||
description: 'Checking JavaScript usage...',
|
||||
icon: Database,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-3',
|
||||
label: 'AI Training Value',
|
||||
description: 'Assessing training potential...',
|
||||
icon: Network,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-4',
|
||||
label: 'Knowledge Extraction',
|
||||
description: 'Analyzing entity definitions...',
|
||||
icon: FileCode,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-5',
|
||||
label: 'Template Quality',
|
||||
description: 'Reviewing semantic structure...',
|
||||
icon: Shield,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-6',
|
||||
label: 'Content Depth',
|
||||
description: 'Measuring content richness...',
|
||||
icon: Zap,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
},
|
||||
{
|
||||
id: 'ai-loading-7',
|
||||
label: 'Machine Readability',
|
||||
description: 'Testing extraction reliability...',
|
||||
icon: Globe,
|
||||
status: 'checking' as const,
|
||||
score: 0,
|
||||
isAI: true,
|
||||
isLoading: true
|
||||
}
|
||||
];
|
||||
|
||||
// Add loading AI tiles with staggered animation immediately
|
||||
placeholderAIChecks.forEach((check, idx) => {
|
||||
setTimeout(() => {
|
||||
setCombinedChecks(prev => [...prev, check]);
|
||||
}, 100 * (idx + 1));
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai-analysis', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
htmlContent: analysisData?.htmlContent || '',
|
||||
currentChecks: checks
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.insights) {
|
||||
// Convert AI insights to CheckItem format with AI flag
|
||||
const aiChecks: CheckItem[] = data.insights.map((insight: any, idx: number) => ({
|
||||
...insight,
|
||||
icon: [Sparkles, Bot, Database, Network, FileCode, Shield, Zap, Globe][idx % 8],
|
||||
description: insight.details?.substring(0, 60) + '...' || 'AI Analysis',
|
||||
isAI: true, // Mark as AI-generated
|
||||
}));
|
||||
|
||||
setAiInsights(aiChecks);
|
||||
|
||||
// Replace loading tiles with real AI tiles
|
||||
setCombinedChecks(prev => {
|
||||
// Remove loading tiles
|
||||
const withoutLoading = prev.filter(c => !(c as any).isLoading);
|
||||
// Add real AI tiles
|
||||
return [...withoutLoading, ...aiChecks];
|
||||
});
|
||||
|
||||
// Calculate enhanced score
|
||||
if (data.insights.length > 0) {
|
||||
const aiScores = data.insights.map((i: any) => i.score || 0);
|
||||
const avgAiScore = aiScores.reduce((a: number, b: number) => a + b, 0) / aiScores.length;
|
||||
const combinedScore = Math.round((overallScore * 0.6) + (avgAiScore * 0.4));
|
||||
setEnhancedScore(combinedScore);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI analysis error:', error);
|
||||
// Remove loading tiles on error
|
||||
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
|
||||
} finally {
|
||||
setIsAnalyzingAI(false);
|
||||
}
|
||||
}}
|
||||
disabled={isAnalyzingAI}
|
||||
className="px-20 py-10 bg-accent-black hover:bg-black-alpha-80 text-white rounded-8 text-label-medium transition-all disabled:opacity-50"
|
||||
>
|
||||
{isAnalyzingAI ? 'Analyzing...' : 'Analyze with AI'}
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Check, X, FileText, Globe, Code, Sparkles, AlertCircle } from "lucide-react";
|
||||
// import { Zap, Shield } from "lucide-react"; // Reserved for future features
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface InlineResultsProps {
|
||||
isAnalyzing: boolean;
|
||||
showResults: boolean;
|
||||
analysisStep: number;
|
||||
url: string;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
const analysisSteps = [
|
||||
"Fetching website content...",
|
||||
"Checking for LLMs.txt...",
|
||||
"Analyzing HTML structure...",
|
||||
"Calculating AI readiness...",
|
||||
];
|
||||
|
||||
// Placeholder data for the results
|
||||
const mockResults = {
|
||||
score: 78,
|
||||
grade: "B+",
|
||||
llmsTxt: true,
|
||||
robotsTxt: true,
|
||||
structuredData: true,
|
||||
semanticHTML: false,
|
||||
metaTags: true,
|
||||
accessibility: true,
|
||||
};
|
||||
|
||||
export default function InlineResults({
|
||||
isAnalyzing,
|
||||
showResults,
|
||||
analysisStep,
|
||||
url: _url, // URL prop available but not used in current implementation
|
||||
onReset,
|
||||
}: InlineResultsProps) {
|
||||
const [displayScore, setDisplayScore] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (showResults) {
|
||||
// Animate score counting up
|
||||
const target = mockResults.score;
|
||||
const duration = 1500;
|
||||
const increment = target / (duration / 16);
|
||||
let current = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
setDisplayScore(target);
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
setDisplayScore(Math.floor(current));
|
||||
}
|
||||
}, 16);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
}, [showResults]);
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 80) return "#22c55e";
|
||||
if (score >= 60) return "#eab308";
|
||||
return "#ef4444";
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{/* Analyzing State */}
|
||||
{isAnalyzing && (
|
||||
<motion.div
|
||||
key="analyzing"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-20"
|
||||
>
|
||||
{/* Progress Bar */}
|
||||
<div className="relative">
|
||||
<div className="h-2 bg-black-alpha-4 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-heat-100 to-heat-200"
|
||||
initial={{ width: "0%" }}
|
||||
animate={{ width: `${((analysisStep + 1) / 4) * 100}%` }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Glowing dot at the end of progress */}
|
||||
<motion.div
|
||||
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-heat-100 rounded-full"
|
||||
style={{
|
||||
left: `${((analysisStep + 1) / 4) * 100}%`,
|
||||
boxShadow: "0 0 20px rgba(255, 77, 0, 0.8)",
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [1, 0.8, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
repeat: Infinity,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status Text */}
|
||||
<motion.div
|
||||
key={analysisStep}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="flex items-center gap-8 text-body-medium text-black-alpha-64"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
>
|
||||
<Sparkles className="w-16 h-16 text-heat-100" />
|
||||
</motion.div>
|
||||
{analysisSteps[analysisStep]}
|
||||
</motion.div>
|
||||
|
||||
{/* ASCII Animation */}
|
||||
<motion.div
|
||||
className="font-mono text-xs text-black-alpha-16 overflow-hidden h-32 relative"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, -10, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
{'< analyzing />'}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Results State */}
|
||||
{showResults && (
|
||||
<motion.div
|
||||
key="results"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
className="space-y-24"
|
||||
>
|
||||
{/* Score Display */}
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
delay: 0.2
|
||||
}}
|
||||
className="relative inline-block"
|
||||
>
|
||||
{/* Background glow */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full blur-xl"
|
||||
style={{ background: getScoreColor(mockResults.score) }}
|
||||
animate={{
|
||||
opacity: [0.3, 0.6, 0.3],
|
||||
scale: [0.8, 1.2, 0.8],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Score circle */}
|
||||
<div
|
||||
className="relative w-120 h-120 rounded-full flex flex-col items-center justify-center"
|
||||
style={{
|
||||
background: `conic-gradient(from 0deg, ${getScoreColor(mockResults.score)} ${displayScore * 3.6}deg, #f0f0f0 ${displayScore * 3.6}deg)`,
|
||||
padding: "4px",
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full bg-white rounded-full flex flex-col items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-4xl font-bold"
|
||||
style={{ color: getScoreColor(mockResults.score) }}
|
||||
>
|
||||
{displayScore}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
className="text-label-medium text-black-alpha-48"
|
||||
>
|
||||
AI Ready
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Quick Checks Grid */}
|
||||
<motion.div
|
||||
className="grid grid-cols-3 gap-12"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
{[
|
||||
{
|
||||
label: "LLMs.txt",
|
||||
value: mockResults.llmsTxt,
|
||||
icon: FileText,
|
||||
description: "AI instructions",
|
||||
detail: mockResults.llmsTxt ? "Found" : "Missing"
|
||||
},
|
||||
{
|
||||
label: "Structured Data",
|
||||
value: mockResults.structuredData,
|
||||
icon: Code,
|
||||
description: "Schema markup",
|
||||
detail: mockResults.structuredData ? "Detected" : "Not found"
|
||||
},
|
||||
{
|
||||
label: "Semantic HTML",
|
||||
value: mockResults.semanticHTML,
|
||||
icon: Globe,
|
||||
description: "HTML5 tags",
|
||||
detail: mockResults.semanticHTML ? "Good" : "Needs work"
|
||||
},
|
||||
].map((item, index) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.9 + index * 0.1 }}
|
||||
className={`
|
||||
relative p-16 rounded-12 transition-all hover:shadow-md cursor-pointer
|
||||
${item.value
|
||||
? 'bg-gradient-to-br from-green-50 to-green-100/50 border-green-200'
|
||||
: 'bg-gradient-to-br from-red-50 to-red-100/50 border-red-200'}
|
||||
border
|
||||
`}
|
||||
>
|
||||
{/* Status indicator */}
|
||||
<div className="absolute top-12 right-12">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 1 + index * 0.1, type: "spring" }}
|
||||
className={`
|
||||
w-24 h-24 rounded-full flex items-center justify-center
|
||||
${item.value ? 'bg-green-500' : 'bg-red-500'}
|
||||
`}
|
||||
>
|
||||
{item.value ? (
|
||||
<Check className="w-14 h-14 text-white" strokeWidth={3} />
|
||||
) : (
|
||||
<X className="w-14 h-14 text-white" strokeWidth={3} />
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="mb-12">
|
||||
<item.icon className={`
|
||||
w-24 h-24
|
||||
${item.value ? 'text-green-600' : 'text-red-600'}
|
||||
`} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-label-medium text-accent-black">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="text-body-small text-black-alpha-48">
|
||||
{item.description}
|
||||
</div>
|
||||
<div className={`
|
||||
text-label-small font-semibold
|
||||
${item.value ? 'text-green-600' : 'text-red-600'}
|
||||
`}>
|
||||
{item.detail}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Quick Tip */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.2 }}
|
||||
className="p-12 bg-heat-4 rounded-8 border border-heat-100 flex items-start gap-8"
|
||||
>
|
||||
<AlertCircle className="w-16 h-16 text-heat-100 mt-2" />
|
||||
<div className="flex-1">
|
||||
<div className="text-label-medium text-accent-black mb-4">Quick Tip</div>
|
||||
<div className="text-body-small text-black-alpha-64">
|
||||
{mockResults.semanticHTML
|
||||
? "Your site has good semantic HTML structure for AI understanding."
|
||||
: "Add semantic HTML5 elements to improve AI comprehension of your content."}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.4 }}
|
||||
className="flex gap-8"
|
||||
>
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="flex-1 px-16 py-10 bg-black-alpha-4 hover:bg-black-alpha-6 rounded-8 text-label-medium transition-all"
|
||||
>
|
||||
Try Another
|
||||
</button>
|
||||
<button className="flex-1 px-16 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-label-medium transition-all shadow-lg hover:shadow-xl">
|
||||
View Details
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
interface MetricBarsProps {
|
||||
metrics: {
|
||||
label: string;
|
||||
score: number;
|
||||
status: 'pass' | 'warning' | 'fail';
|
||||
category?: 'page' | 'domain' | 'ai';
|
||||
details?: string;
|
||||
recommendation?: string;
|
||||
actionItems?: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function MetricBars({ metrics }: MetricBarsProps) {
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
|
||||
const getBarColor = (score: number) => {
|
||||
// Use brand orange colors with opacity for gradient effect
|
||||
if (score >= 80) return 'bg-heat-100';
|
||||
if (score >= 60) return 'bg-heat-90';
|
||||
if (score >= 40) return 'bg-heat-40 opacity-80';
|
||||
return 'bg-heat-20';
|
||||
};
|
||||
|
||||
const getBulletColor = (_score: number) => {
|
||||
// Always use heat-100 for all bullets for consistency
|
||||
return 'bg-heat-100';
|
||||
};
|
||||
|
||||
const toggleExpanded = (label: string) => {
|
||||
const newExpanded = new Set(expandedItems);
|
||||
if (newExpanded.has(label)) {
|
||||
newExpanded.delete(label);
|
||||
} else {
|
||||
newExpanded.add(label);
|
||||
}
|
||||
setExpandedItems(newExpanded);
|
||||
};
|
||||
|
||||
// Sort metrics by score descending
|
||||
const sortedMetrics = [...metrics].sort((a, b) => b.score - a.score);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-[800px] mx-auto">
|
||||
{sortedMetrics.map((metric, index) => {
|
||||
const isExpanded = expandedItems.has(metric.label);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={metric.label}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05, duration: 0.3 }}
|
||||
className="space-y-0"
|
||||
>
|
||||
<div
|
||||
className={`grid grid-cols-12 gap-4 items-center p-8 -m-8 rounded-8 cursor-pointer transition-all hover:bg-black-alpha-2 ${
|
||||
isExpanded ? 'bg-black-alpha-4' : ''
|
||||
}`}
|
||||
onClick={() => toggleExpanded(metric.label)}
|
||||
>
|
||||
{/* Bullet and Label - fixed width */}
|
||||
<div className="col-span-4 flex items-center gap-8">
|
||||
<div className={`w-6 h-6 rounded-full ${getBulletColor(metric.score)}`} />
|
||||
<span className="text-label-medium text-accent-black truncate">{metric.label}</span>
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="ml-auto"
|
||||
>
|
||||
<ChevronDown className="w-16 h-16 text-black-alpha-32" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bar container - flexible width */}
|
||||
<div className="col-span-7 relative">
|
||||
<div className="relative h-8 bg-black-alpha-8 rounded-full overflow-hidden">
|
||||
{/* Animated bar */}
|
||||
<motion.div
|
||||
className={`absolute inset-y-0 left-0 ${getBarColor(metric.score)} rounded-full`}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${Math.max(metric.score, 2)}%` }}
|
||||
transition={{
|
||||
delay: 0.2 + index * 0.05,
|
||||
duration: 0.8,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
{/* Subtle inner glow */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-transparent to-white opacity-10 rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
{/* Score indicator lines at key thresholds */}
|
||||
{[40, 60, 80].map(threshold => (
|
||||
<div
|
||||
key={threshold}
|
||||
className="absolute top-0 bottom-0 w-px bg-black-alpha-8 opacity-30"
|
||||
style={{ left: `${threshold}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score value - fixed width */}
|
||||
<div className="col-span-1 text-right">
|
||||
<span className="text-label-medium font-medium text-heat-100">
|
||||
{metric.score}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Details */}
|
||||
<AnimatePresence>
|
||||
{isExpanded && metric.details && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="pl-54 pr-12 py-12 space-y-8">
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-48 mb-4">Status</div>
|
||||
<div className="text-body-small text-accent-black">{metric.details}</div>
|
||||
</div>
|
||||
{metric.recommendation && (
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-48 mb-4">Recommendation</div>
|
||||
<div className="text-body-small text-black-alpha-64">{metric.recommendation}</div>
|
||||
</div>
|
||||
)}
|
||||
{metric.actionItems && metric.actionItems.length > 0 && (
|
||||
<div>
|
||||
<div className="text-label-small text-black-alpha-48 mb-4">Action Items</div>
|
||||
<ul className="space-y-4">
|
||||
{metric.actionItems.map((item: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-6 text-body-small text-black-alpha-64">
|
||||
<span className="text-heat-100 mt-1">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Summary stats */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="mt-20 pt-12 border-t border-black-alpha-8"
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-16 text-center">
|
||||
<div>
|
||||
<div className="text-title-h3 text-heat-150">
|
||||
{metrics.filter(m => m.status === 'pass').length}
|
||||
</div>
|
||||
<div className="text-label-small text-black-alpha-48">Passing</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-title-h3 text-heat-100">
|
||||
{metrics.filter(m => m.status === 'warning').length}
|
||||
</div>
|
||||
<div className="text-label-small text-black-alpha-48">Warning</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-title-h3 text-heat-50">
|
||||
{metrics.filter(m => m.status === 'fail').length}
|
||||
</div>
|
||||
<div className="text-label-small text-black-alpha-48">Failing</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface RadarChartProps {
|
||||
data: {
|
||||
label: string;
|
||||
score: number;
|
||||
maxScore?: number;
|
||||
}[];
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function RadarChart({ data, size = 300 }: RadarChartProps) {
|
||||
const [isAnimated, setIsAnimated] = useState(false);
|
||||
const center = size / 2;
|
||||
const radius = (size / 2) - 60; // Increased padding for labels
|
||||
const angleStep = (Math.PI * 2) / data.length;
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimated(true);
|
||||
}, []);
|
||||
|
||||
// Calculate points for the polygon
|
||||
const getPoint = (value: number, index: number) => {
|
||||
const angle = index * angleStep - Math.PI / 2;
|
||||
const r = (value / 100) * radius;
|
||||
return {
|
||||
x: center + r * Math.cos(angle),
|
||||
y: center + r * Math.sin(angle)
|
||||
};
|
||||
};
|
||||
|
||||
// Create polygon points string
|
||||
const polygonPoints = data
|
||||
.map((item, i) => {
|
||||
const point = getPoint(isAnimated ? item.score : 0, i);
|
||||
return `${point.x},${point.y}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
// Grid levels
|
||||
const gridLevels = [20, 40, 60, 80, 100];
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg width={size} height={size} className="overflow-visible">
|
||||
<defs>
|
||||
<linearGradient id="radar-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#FF4A00" stopOpacity="0.8" />
|
||||
<stop offset="100%" stopColor="#FF8533" stopOpacity="0.3" />
|
||||
</linearGradient>
|
||||
<filter id="radar-glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Grid circles */}
|
||||
{gridLevels.map((level) => (
|
||||
<circle
|
||||
key={level}
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={(level / 100) * radius}
|
||||
fill="none"
|
||||
stroke="rgba(0,0,0,0.05)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Axis lines */}
|
||||
{data.map((_, i) => {
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const x2 = center + radius * Math.cos(angle);
|
||||
const y2 = center + radius * Math.sin(angle);
|
||||
return (
|
||||
<line
|
||||
key={i}
|
||||
x1={center}
|
||||
y1={center}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke="rgba(0,0,0,0.05)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Data polygon */}
|
||||
<motion.polygon
|
||||
points={polygonPoints}
|
||||
fill="url(#radar-gradient)"
|
||||
stroke="#FF4A00"
|
||||
strokeWidth="2"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
filter="url(#radar-glow)"
|
||||
/>
|
||||
|
||||
{/* Data points */}
|
||||
{data.map((item, i) => {
|
||||
const point = getPoint(item.score, i);
|
||||
return (
|
||||
<motion.circle
|
||||
key={i}
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r="4"
|
||||
fill="#FF4A00"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: isAnimated ? 1 : 0 }}
|
||||
transition={{ delay: 0.8 + i * 0.1, duration: 0.3 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Labels */}
|
||||
{data.map((item, i) => {
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const labelRadius = radius + 40; // Increased label distance
|
||||
const x = center + labelRadius * Math.cos(angle);
|
||||
const y = center + labelRadius * Math.sin(angle);
|
||||
|
||||
// Better text anchor logic based on quadrant
|
||||
let textAnchor = "middle";
|
||||
let dy = 0;
|
||||
|
||||
// Left side
|
||||
if (x < center - 20) {
|
||||
textAnchor = "end";
|
||||
}
|
||||
// Right side
|
||||
else if (x > center + 20) {
|
||||
textAnchor = "start";
|
||||
}
|
||||
|
||||
// Top
|
||||
if (y < center - 20) {
|
||||
dy = -5;
|
||||
}
|
||||
// Bottom
|
||||
else if (y > center + 20) {
|
||||
dy = 5;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.g key={i}>
|
||||
{/* Background for better readability */}
|
||||
<motion.rect
|
||||
x={x - (textAnchor === "middle" ? 30 : textAnchor === "end" ? 60 : 0)}
|
||||
y={y - 10}
|
||||
width={60}
|
||||
height={20}
|
||||
fill="white"
|
||||
fillOpacity={0.9}
|
||||
rx={4}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.9 + i * 0.05 }}
|
||||
/>
|
||||
<motion.text
|
||||
x={x}
|
||||
y={y + dy}
|
||||
textAnchor={textAnchor as any}
|
||||
dominantBaseline="middle"
|
||||
className="text-xs fill-black-alpha-80 font-medium"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 + i * 0.05 }}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{item.label}
|
||||
</motion.text>
|
||||
{/* Score value */}
|
||||
<motion.text
|
||||
x={x}
|
||||
y={y + dy + 12}
|
||||
textAnchor={textAnchor as any}
|
||||
dominantBaseline="middle"
|
||||
className="text-[10px] fill-heat-100 font-bold"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.1 + i * 0.05 }}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{item.score}%
|
||||
</motion.text>
|
||||
</motion.g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-16 flex justify-center">
|
||||
<div className="inline-flex flex-row gap-16 text-xs text-black-alpha-48 bg-white px-16 py-8 rounded-6 shadow-sm">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-12 h-12 rounded-full bg-heat-200" />
|
||||
<span className="whitespace-nowrap">80-100%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-12 h-12 rounded-full bg-heat-100" />
|
||||
<span className="whitespace-nowrap">60-79%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-12 h-12 rounded-full bg-heat-50" />
|
||||
<span className="whitespace-nowrap"><60%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ScoreChartProps {
|
||||
score: number;
|
||||
enhanced?: boolean;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function ScoreChart({ score, enhanced = false, size = 200 }: ScoreChartProps) {
|
||||
const [animatedScore, setAnimatedScore] = useState(0);
|
||||
const radius = size / 2 - 20;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setAnimatedScore(score);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [score]);
|
||||
|
||||
// Calculate stroke dash offset for the progress
|
||||
const strokeDashoffset = circumference - (animatedScore / 100) * circumference;
|
||||
|
||||
// Determine color based on score
|
||||
const getColor = () => {
|
||||
if (score >= 80) return "#FF4A00"; // heat-200 - Excellent
|
||||
if (score >= 60) return "#FF6500"; // heat-150 - Good
|
||||
if (score >= 40) return "#FF8533"; // heat-100 - Warning
|
||||
return "#FFA566"; // heat-50 - Poor
|
||||
};
|
||||
|
||||
const getGradientId = enhanced ? "enhanced-gradient" : "normal-gradient";
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex items-center justify-center">
|
||||
<svg width={size} height={size} className="transform -rotate-90">
|
||||
<defs>
|
||||
<linearGradient id={getGradientId} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor={getColor()} stopOpacity="1" />
|
||||
<stop offset="100%" stopColor={enhanced ? "#FF8533" : getColor()} stopOpacity="0.6" />
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="rgba(0,0,0,0.05)"
|
||||
strokeWidth="12"
|
||||
/>
|
||||
|
||||
{/* Progress circle */}
|
||||
<motion.circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={`url(#${getGradientId})`}
|
||||
strokeWidth="12"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
initial={{ strokeDashoffset: circumference }}
|
||||
animate={{ strokeDashoffset }}
|
||||
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||
filter="url(#glow)"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Center content */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<motion.div
|
||||
className="text-4xl font-bold text-heat-150"
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5, duration: 0.5 }}
|
||||
>
|
||||
{animatedScore}%
|
||||
</motion.div>
|
||||
{enhanced && (
|
||||
<motion.div
|
||||
className="text-xs text-heat-100 mt-1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
AI Enhanced
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsCrawl({
|
||||
active,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeGroup = 0;
|
||||
const rowAlphas = [0.2, 0.4, 1, 0.04];
|
||||
|
||||
const grid = [
|
||||
[24],
|
||||
[16, 18, 30, 32],
|
||||
[8, 12, 36, 40],
|
||||
[0, 3, 6, 21, 27, 42, 45, 48],
|
||||
];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (const group of grid.slice(0, 4)) {
|
||||
const groupIndex = grid.indexOf(group);
|
||||
ctx.globalAlpha = rowAlphas[groupIndex];
|
||||
|
||||
for (const index of group) {
|
||||
ctx.fillRect(
|
||||
(3 + (index % 7) * 2) * scaler,
|
||||
(3 + Math.floor(index / 7) * 2) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeGroup = (activeGroup + 1) % 5;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeGroup) targetAlpha = 1;
|
||||
else if (index === (activeGroup + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeGroup + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeGroup + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 300),
|
||||
);
|
||||
|
||||
if (activeGroup === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeGroup === 2) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [triggerOnHover, size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsExtract({
|
||||
active,
|
||||
disabledCells,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
disabledCells?: number[];
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeCol = 0;
|
||||
const colAlphas = [1, 0.4, 0.2, 0.12];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw Extract pattern - represents structured data extraction
|
||||
// Draw columns to represent data fields
|
||||
for (let col = 0; col < 4; col++) {
|
||||
ctx.globalAlpha = colAlphas[col];
|
||||
|
||||
// Draw vertical bars of different heights to represent extracted data
|
||||
const heights = [3, 2, 3, 1];
|
||||
const startY = [1, 2, 1, 3];
|
||||
|
||||
for (let row = 0; row < heights[col]; row++) {
|
||||
ctx.fillRect(
|
||||
(3 + col * 4) * scaler,
|
||||
(3 + startY[col] * 2 + row * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeCol = (activeCol + 1) % 4;
|
||||
|
||||
colAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeCol) targetAlpha = 1;
|
||||
else if (index === (activeCol + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeCol + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeCol + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
colAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 400),
|
||||
);
|
||||
|
||||
if (activeCol === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeCol === 0) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [disabledCells, size, triggerOnHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
|
||||
|
||||
export default function EndpointsMap(
|
||||
props: ComponentProps<typeof EndpointsScrape>,
|
||||
) {
|
||||
return <EndpointsScrape {...props} disabledCells={[1, 2, 3, 7, 9, 12, 15]} />;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsScrape({
|
||||
active,
|
||||
disabledCells,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
disabledCells?: number[];
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeRow = 2;
|
||||
const rowAlphas = [0.2, 0.4, 1, 0.12];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if (disabledCells && disabledCells.includes(i)) continue;
|
||||
|
||||
ctx.globalAlpha = rowAlphas[Math.floor(i / 4)];
|
||||
|
||||
ctx.fillRect(
|
||||
(3 + (i % 4) * 4) * scaler,
|
||||
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeRow = (activeRow + 1) % 5;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeRow) targetAlpha = 1;
|
||||
else if (index === (activeRow + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeRow + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeRow + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 400),
|
||||
);
|
||||
|
||||
if (activeRow === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeRow === 2) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [disabledCells, size, triggerOnHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function EndpointsSearch({
|
||||
alwaysHeat,
|
||||
size = 20,
|
||||
}: {
|
||||
alwaysHeat?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let diff = 0;
|
||||
const defaultRowAlphas = [
|
||||
0, 0.2, 0.4, 0, 0.4, 1, 0.4, 0.2, 0.2, 0.4, 1, 0.4, 0, 0.4, 0.2, 0,
|
||||
];
|
||||
|
||||
const differs = Array.from({ length: 16 }, () => 0.2 + Math.random() * 0.2);
|
||||
|
||||
differs[5] = 0.6;
|
||||
differs[6] = 0.6;
|
||||
differs[9] = 0.6;
|
||||
differs[10] = 0.6;
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if ([0, 3, 12, 15].includes(i)) continue;
|
||||
|
||||
const maxAlpha = [5, 6, 9, 10].includes(i) ? 1 : 0.4;
|
||||
|
||||
const alpha = defaultRowAlphas[i] + diff * differs[i];
|
||||
ctx.globalAlpha = Math.min(
|
||||
Math.min(alpha, maxAlpha) - Math.max(alpha - maxAlpha, 0),
|
||||
1,
|
||||
);
|
||||
|
||||
ctx.fillRect(
|
||||
(3 + (i % 4) * 4) * scaler,
|
||||
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const duration = 300;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
|
||||
animate(diff, 1, {
|
||||
duration: duration / 1000,
|
||||
onUpdate: (value) => {
|
||||
diff = value < 0.5 ? value * 2 : 1 - (value - 0.5) * 2;
|
||||
},
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(
|
||||
() => {
|
||||
isRunning = false;
|
||||
},
|
||||
Math.max(duration, 300),
|
||||
),
|
||||
);
|
||||
|
||||
runCount += 1;
|
||||
|
||||
if (runCount === 3 || !isActive) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, duration),
|
||||
);
|
||||
};
|
||||
|
||||
const activate = () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", activate);
|
||||
group.addEventListener("mouseleave", deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", activate);
|
||||
group.removeEventListener("mouseleave", deactivate);
|
||||
};
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
return <canvas ref={canvasRef} style={{ width: size, height: size }} />;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsExtract({ size = 20 }: { size?: number }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let diff = 0;
|
||||
const defaultRowAlphas = [
|
||||
0.4, 0.04, 0.2, 0.4, 0.2, 0, 0, 0.04, 0.04, 0, 0, 0.2, 0.4, 0.2, 0.04,
|
||||
0.4,
|
||||
];
|
||||
|
||||
const differs = Array.from({ length: 16 }, () => 0.2 + Math.random() * 0.2);
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if ([5, 6, 9, 10].includes(i)) continue;
|
||||
|
||||
ctx.globalAlpha = defaultRowAlphas[i] + diff * differs[i];
|
||||
ctx.globalAlpha =
|
||||
Math.min(ctx.globalAlpha, 0.4) - Math.max(ctx.globalAlpha - 0.4, 0);
|
||||
|
||||
ctx.fillRect(
|
||||
(3 + (i % 4) * 4) * scaler,
|
||||
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillRect(7, 7, 6, 2);
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.fillRect(7, 11, 2 + diff * 4, 2);
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const duration = 300;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
|
||||
animate(diff, 1, {
|
||||
duration: duration / 1000,
|
||||
onUpdate: (value) => {
|
||||
diff = value < 0.5 ? value * 2 : 1 - (value - 0.5) * 2;
|
||||
},
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(
|
||||
() => {
|
||||
isRunning = false;
|
||||
},
|
||||
Math.max(duration, 300),
|
||||
),
|
||||
);
|
||||
|
||||
runCount += 1;
|
||||
|
||||
if (runCount === 3 || !isActive) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, duration),
|
||||
);
|
||||
};
|
||||
|
||||
const activate = () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", activate);
|
||||
group.addEventListener("mouseleave", deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", activate);
|
||||
group.removeEventListener("mouseleave", deactivate);
|
||||
};
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
return <canvas ref={canvasRef} style={{ width: size, height: size }} />;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsMcp({
|
||||
active,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeIndex = 5;
|
||||
const rowAlphas = [0.12, 0.2, 0.4, 0.4, 1, 1, 1, 0.4, 0.2];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
ctx.globalAlpha = rowAlphas[i];
|
||||
|
||||
ctx.fillRect(
|
||||
(5 + (i % 3) * 4) * scaler,
|
||||
(5 + Math.floor(i / 3) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeIndex = (activeIndex + 1) % 9;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeIndex) targetAlpha = 1;
|
||||
else if (index === (activeIndex - 1 + 9) % 9) targetAlpha = 1;
|
||||
else if (index === (activeIndex - 2 + 9) % 9) targetAlpha = 1;
|
||||
else if (index === (activeIndex - 3 + 9) % 9) targetAlpha = 0.4;
|
||||
else if (index === (activeIndex - 4 + 9) % 9) targetAlpha = 0.2;
|
||||
else if (index === (activeIndex - 5 + 9) % 9) targetAlpha = 0.2;
|
||||
else if (index === (activeIndex - 6 + 9) % 9) targetAlpha = 0.12;
|
||||
else if (index === (activeIndex - 7 + 9) % 9) targetAlpha = 0.12;
|
||||
else if (index === (activeIndex - 8 + 9) % 9) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 30 / 1000,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 300),
|
||||
);
|
||||
|
||||
if (activeIndex === 7) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeIndex === 6) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 30),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [size, triggerOnHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
|
||||
import data from "./data.json";
|
||||
|
||||
export default function HeroFlame() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ref2 = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = 0;
|
||||
|
||||
ref.current!.innerHTML = data[index];
|
||||
ref2.current!.innerHTML = data[index];
|
||||
},
|
||||
interval: 85,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cw-686 h-190 top-408 absolute flex gap-16 pointer-events-none select-none"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -left-380 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -right-380 -scale-x-100 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref2}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
[
|
||||
" \n \n \n \n \n . . \n .. ..+ \n .:. \n .. .. .:: \n +.. ..: :. \n .:..::. .. .. \n .--:::. .. ... .:. .. \n .. .:+=-::.:. . ...-.::. .. \n ::.... .:--+::..: ......:+....:. :.. .. \n ....... ::-=:::: ..:-:-...: .--..:: ......... \n .. . . . ..::-:-.. .-+-:::.. ...::::. .: ...::.:.. \n . -... ....: . . .--=+-::. :-=-:.... . .:..:: .:---:::::-::.... \n ..::........::=..... ...:-.. .:-=--+=-:. ..--:..=::.... . .:.. ..:---::::---=:::..:... \n ..........::::.:::::::-::.-.. ...::--==:. ..-::-+==-:... .-::....... ..--:. ..:=+==.---=-+-:::::::-.. \n . .....::......:: ::::-::.---=+-:..::-+==++X=-:. ..:-::-=-== ---.. .:.--::.. .:-==::=--X==-----====--::+:::+... \n ..-....-:..::-::=-=-:-::--===++=-==-----== X+=-:.::-==----+==+XX+=-::.:+--==--::. .:-+X=----+X=-=------===--::-:...:. .... \n ....::::...:-:-==+++=++==+++XX++==++--+-+==++++=-===+=---:-==+X:XXX+=-:-=-==++=-:. .:-=+=- -=X+X+===+---==--==--:..::...+....+ \n ..:::---.::.---=+==XXXXXXXX+XX++==++===--+===:+X+====+=--::--=+XXXXXXX+==++==+XX+=: ::::--=+++X++X+XXXX+=----==++.+=--::+::::+. ::.=... \n .:::-==-------=X+++XXXXXXXXXXX++==++.==-==-:-==+X++==+=-=--=++++X++:X:X+++X+-+X X+=---=-==+=+++XXXXX+XX=+=--=X++XXX==---::-+-::::.:..-..\n",
|
||||
" \n \n \n \n .. \n . .+. \n \n .: \n : .. :. \n .. ... .. .. \n :...+. . .. :. . \n .=-::... . . ... .. \n .. .--=-::... .....=+.:. . . \n -:.... .:-=:...: .::...... .:. .. . ... \n .. .. . .:: :.:: .:-::.. . .-..:.: ........... \n ..= . .. .::-==.. .-=-::... ..:.. . ..:::.::.:... \n .+.:.:. ..-.:: . . ..:. .:=-==-::. .:--.. .. .. ... .:---:::::--::..... \n .. ..+::.......-::..: . ::--.. ..:::-==-:. ..::.. .:... ..-.. =..:=== ::::-+-=:...+.=.. \n .....= ....:::..:::::- ::=:. ..:=--==+:. .:-::-=-=--:... .:-..... .:-=:...-+==--:+:-+-=-:-:.:+... \n .....:...::.:: ::::-::----=+-::--:---+=XX=-:. ..-=-=::--==X==-:....-.:--::.. .::++-::--+=---:-:---=-=::...-.. \n ....:..:....:=:- ==--=-:--===++====---- -==+==-::--==-:-::--=XX ++=-:::---+===-:. .:-X+ ----=X==----: :=--.--::........... \n .+.:::::-..:-:-===++=++=++++X++=====-.--=X==++==----=--::+:-=+XXXXX++---=-==+++=:. ..:-+++---=+XX+++=-::-===-+=--:...:.......... \n ...::--- ::::--=+=+XXXXXXXX+++=====+===--=---==++==-=+=--::-==++XX+XXX++=+X==+XX+=-::-::--==++X++XXXXXXX=-::-+=++X+=-:::::::::-:=+..... \n ..: :-===----=-=+++++XX+XX-XX++X+==++==+=--:--==.XX+==++.===+++XX.++++XX++=X+=++XX+==--=--===+ +XXXXXXXXX+=--=X++.XX==--=-:---:::::::..:.\n",
|
||||
" \n \n . \n . \n \n . : \n . .. \n . .. . \n . \n :.. . . \n .::... . \n ==-:: : . \n . . .-.=::. .:: ..+ . .=. \n . :.. ..+:..:-. .:.. .: .+. . .. \n . . .:.:-:. .-=-... ..-. ...:.. .. .. \n . . .. .. :. ..::--:.. .::-. . .: .:::-:....-...:. \n .. ......:: ::. .::+:::: .:=::==:.. ...... ... . . .::----:...::::::.... \n . .:.....:::::=:-. ..::--=-.. :-::--==.::.. . .: .. .::-:..:==--::..:::::.:.....: . \n +.. ...:-+...:.: :=---::..:::::-=+=:.. .::.:::--.++--:.. ..-:..+. .:=-::::==-:::::::-=-:.::... . \n -... .. .. .:---:+:-:-::-=+==--=-::.::-X=--::..::::.:.:--=XX+==--::::.:-+=-:. ..-=-::-::=++-::-::.:---::.. . \n ......... .:::-=+== -==+= =====--=-:::::-+=+=--:.:::..::.:-===XXX+=--::--+==+=-. ..-==-:.:-=XX=----:.:-=-=--:.. .+..... \n ...:::+:...:=:-+++XX++++.=-=-==-- ===-:.:=---=+=-:::--::..:-==++X+X+==--=+==+X+-:.......:-==++==++X:X++=-:::-==+=--:.......::::..+. \n ...:---::+:::--==++-XX++XX+=:=++=--====-:::+--==+=---== ---==++XXX++XX+.+XX===+X+=-:::-:--=====XX+-++XXX+-:--++=+X+--::::.:::::::..... \n ..::.-==---+- ++++.+++XXX-XX++:X+=-=X+==:-::.=+X+XXX+=+X++++XXXXXXXX=XX=- +++-==++X+==-==-=:=++XXXXXXXXXX++==+ ++X X+=---.--.-:.:::.:... \n",
|
||||
" \n \n . \n : \n . \n . \n . \n \n \n :. . \n :-....:. .- \n ....... .. \n . .. .. :. \n .. ..:.:. .-.. . ...:... \n . .. ..... .:--:.. .::... . . .. ........ .:. . \n .. ..... ...: ..=..::-. .:.:=-:. . ... .. .:.:..:-...-:......... \n . .:: ..::.:.:.. . .:.::-.. ..=.:--==:.. . .::..+==::::+........ . \n .:.......:.:::.....:::..::==:.. :.....::-+X-:::. .-.:... ..-:..::=::-:-...:.:::... \n .. :::.:.-.:::-=---::=::...:--==-:. ..= ....:-++=--=-::.+...==-::. .--:....:=-:-:::.... ::.. \n +.... .::-==+-----------------:::..-==--::. ... ..::-=+:+++=-:::.-- =+=-:. ..=--:::::=++-=::-::.:=-::... .. \n ..:-.... :..:-+++=+=-===-------::--.::..:------:... :....::-=++XX+=--::----+X-:. ..-==-----+X+==-=-:::-=+=-::.. .....:...- \n ...:::::....:-- ==+X+====-----=+::-==-:::::----=-:.::-::::--=+XXXXXX+=-=+=-==++--:-:::.::=--=-==X++++X+=-:=:===++--::......:...... \n ...:----:::--.====+=++X+++======---=+=--:-::=+++X++=-====== ++XXXX.XXX +++-=--=+X+--::--:---==+XX+++XXXX+=--=+=++X+=-:::-::-:::....... \n .. ...:::--==:---++=+=====++ XXXXX++===+X+===+==-+X++++++++X+X++.XXXXXXXXXXXX ++======X+==-=--===+XXXXXXXXXXXX+=+++++X+XX=.--==--::-....:....\n",
|
||||
" . \n . \n \n \n \n \n \n \n . ..: \n :. . .. \n ::.. \n . .. \n .: :::. :. .. \n .:..:. .:-. ::. . .: .. ..... \n ... . ... . . ... ..::-:. . . ... .:........:. \n .. =.... . ... .. .:.:-+-.. ....-=:....:.=... .. \n ... -..=::... . ......:::-: .::=+-:::+. ::-.. .:..::=:...:.......... \n ....:. ...:....:::=:.:-.. .::=--: ..:==:-::::. . .:---:. .:=:::. :::-.-...:...=. \n ..:.--::::::::::::::--:-:....--:::... ..:-==++---:.=.-::-=+-:. .:::.....--=-:.-.::.--:... \n ....... ..-=---==-=-==-::.-::+:--::..-:---:::....... ..---=+XXX+=-+:::--+X-:.. :--:.::::-++-::::::::==-:..: +.... \n ..:...:....:-=-=+:+=--==::--::-=::--:...:--.:-::....::::---+=+XXX.+=-:-----+=-::..... .::--:---=+++==--:..:-==+-:::.. ......+-. \n ..:-::::..::---=====+=== -:--:--::-==-::-::-==-==----=--::-==+:XXXXXX+==+=--==+=::..:::.::---++.+=+++X+=-:=:===+=--::...:..::...:. \n ....:-------==-=+=====XX+X+==------+=---=:==+=.==+=+++++X+==+XXXXXXXXXXX+++==--==+=-:-:-:---=X XX++XXXXX++===+==+++=------::: :....... \n .....-.:+:--==+======= ++=++X:XXXX+=:==XX==:++=-=++==+X+=+++:+XXXXX:-X.XXXXX+X++==-++++X+======++XXXXX-XXXX--XXX+==+++++X++==.---::::..=:. ..\n",
|
||||
" \n \n \n \n \n \n \n . \n . . \n :. \n .. . .. \n . .. ::. . . \n . . .:+.. : .. . \n . . . ..-- .. .. ..: \n . .. .. . ...-:... . ...:-.. ... \n . ....-.-. ..:.. .. :: ...-::...... .. .. .:.:::=....:. .. \n -... .....:...:.:-. ...=.:: .:=-::::::. .::--. ....:.:. ..:.. ..: .. \n .::::::.:..+:....:..-=:-:. ..:..::. ..=------::...:..:=-.. ........::-:.. ..::.:. \n . . :::----:::::=::....+:=-:-..-::-::-... . .:-==++++++-:.-::-+-... .::.... .:==::.....:--+:.. . . \n .... .. .:-=-=+==---:-:=:::..::::-:..:-:::::::....=.:..::--=+XXX-=-:-:-:=--::.. :.::..:::-++--::...::==-:.:.. ....+. \n ...-.:.. ...:-==--=+--:-::::..::.:--:..:=:-+-:---::--:::+:-==++X:XX+-==--:--==-::.... ..::::-++==++:-::..:-==-:::.. .....+=. . \n -..:::::::--::--==--++=+==--::::::-=-:: =-=-- --=+=+++=++-==++XXXXX- X+==+-----==::..:-.::--=+.++=++XX++=-:-======:--::.:.-...:... \n ......:=:--=----=- -=-===XX+XX+=----.-+=-:-==-+==-.-=+=====-+ X+XXXXXXX:XXX+========++=------+++XXX+.+X=XXX.XX+======+++---:::-.:.=..:.... \n .......::::-=:===++ ==++=++XXXXXX:++++++=====++=+X++:=++=====++XXXXXXXXXXXXX+X++-===++XXX++=+==+X++XX++X-XXXX-XXX+==+++=+X:===-+-:::=..::....\n",
|
||||
" \n \n \n \n \n \n \n \n \n . .. \n ...... \n ..::. \n : . . . \n ... : ....+: . . .. \n . .:. .... . .:. .... . ...:. ..-:.. .. \n . . ..- ...=.: ..:. .:-::::..=.. .:-. ...... .:. :.. \n ....::. .. -.......=: :::. ...-.. .- --+::::---.. ..:.. . .. +..::. ...:. \n . .:::::-:....::-........---:-...::..:. ....:--=--===+=:....:-.... .. .. .:-:.. ..::. . \n . .:::--:--::=:::::. ..::-: ..::-....: ..:......:--===+=X=-::.---::-.. ..:....::.==-::.. :---... . .-. \n .. ...::-:-:--::-::-::. ....::-...----:::::.:=:::-:::-====+XX+=--:::=--::. .::::-=== =+=::....:=-:..-. .... \n .+...... :::::-----=.--:-::...=..:=:...:---::.:--==+--=+:--+:+XXXXXXX=-==:+:---:::...:...-:--++===+XX=-::::=----:::...:...... . \n .....:=:- :::-:::-+--++===+=-::-.::-=:::-=+=--:.::-==+-:=++ +XXXXXXXXX++==+=-=+--=--::::::---+++==++XX-X+=+ ==---===--::.+....=.... \n .=....:.::.==--:-==--====XX XXX++=++=== ==--==+X+==---------==+XXXXXXXXXXXX++==== =+==++-=.=-=+++:=++=++XXXXXXXXX+=====++---:-::..:...:.... \n ......:::--=++= ====-===++XX-X XXXXX:X+====-==+XX:+++========+XXXXXXXXXXXXXXX++=++XX +++++=+++X:X+XX.++XXXXX .XX+++=:+++X+=.---::::...:=:...\n",
|
||||
" \n \n \n \n \n \n \n \n .: . \n . .... \n \n :. ... \n . . ..:. . \n . ..:.. .. .:: .. . \n . .. .:. ..: .:...... .:.. .. :.. .. \n ..-::: .:... .:=:. .. :. ..::=.:: :::=:. . .=. .. . \n ......:.. ..:-.. .. .:-:.. :.... . . .::=-------+-:......:. .. .+.:. .. \n ...::......-:.::. ..:-:....:::.. .: ....:--=--=-=+-:.::::.... .. .::-=-::. :.... \n ..:::.:.::..:..::.. ....--=....=::=:.:...-..:-:.::==--=+++--::---:.... ...::-:--=+:.. .:.:.. . \n .....:: ..:-::--:+:.:-.. .:=..:-..=.--:....::-+-::=-:-.-=++==+XX+=-=-::-::... ... :.::-=-=++X=-:..:::-::.:....... \n ....::.........---====--=-::.:=:.:--=::-=.::....::--::==== XX +-++XX++==.-=-:. :::-..:...:-- =-.==+X++------------:..... .. .. \n .....+.-:::::::-:::+--=X++ ++=====+==---:--=X+=-::.::::--:--=+XXXXXX++X++-====--==:--==--=:-=-=+==+-==+X.XX++++:=----=-::..:.. ....=.. \n . .. ...::+-==------:--==XXXXXXXXXXXX+=------+++X+===--=-----==+XX-XXXXXX:-++==+=++=--==++=++XX++==++==++XXXX:X++++=-===+=--:+::.....=-.... \n ......::--==.+++=+===.=++XXXXXXXXXXXX=++=.=--=+++X+++==+=====+X-X-XXXXXXX X+++++++++++===:==:++XX+XXX++XXXXXXXX.+++++XX+XX+=-=-:::+:::.:.:..\n",
|
||||
" \n \n \n \n \n \n \n . .. \n . \n .. \n . \n .. .:. ... \n ... ...... .. . \n . .. :. . ..:. .:: :. \n +... . ..:. . . .. .:.:..:=:. . . . \n .. ..: .:: ... . ..::-::::.:.:=-. ... .. ...::... \n .:.... ..+.: . ::. .::.. . ..::.::::==::--:..:.. . . .::-:-:. . \n . ........... .-.. .. .:. .-:...... .. ....::-=-::--=-::.:::. .. ....:+:-==.. .. .. \n :...... .:.-:.+....-....:. ..:: .::-.. ..::-:..=::::--+=--==+==-=-:.:...=. ...::---+=::... :::......... \n .. ...- ......::==----:-::.::=::+:-:.::-:.... ...-:::---=+==+:====+++++=:: . ...:: .-. .=.::--=-+=--::::::=-.:::. \n .......:......:..:--X+==:=+=:--=-=-:::--==+=--:....::-:.::-=XXXX++++X+===-:-::-:::.-:::-:=::-=------==++==-=+==-::+::.. .. .. \n +:......:-- ::-::::--+XXXXX++===--=-=-::-+++++==--::::::::-=+XXXXXXXX++==-==-----::--=++++++===--=---==+X++++====----=-:..... ......... \n .....:::::-=+==-+=+--==+X-XXXXX+=X+==--=---==+XX+==+--------=++X+XXXXX+XX++=-==++==------=++++X+=+++==++XXXX=X++==+===:+== :--:............\n .......:--.-====++.X+=++XXXXXXXXXXXXX+==---.===X+.+X+X++=.++++=XXX.XXXX+XX=X++++ +XX+X+==--====+XX+XXX XXXX XXX X+=+X++++++===-=-: :::=::....\n",
|
||||
" \n \n \n \n \n \n . \n \n \n . \n .. \n .. ..... .... \n . .. ..: .::. . \n . .: . .... .:-. .. \n . ::. .=.:..::.. .-=. .. .. -.. \n ..: .: :..= .: :-::+:::.:-::-... .. ..:-:-:. \n . . ..:: .. .: .-:.. . ..+..::--:::--:::=:.. . .. ..=::=-. . \n .......::... ..:.=. .. .: .:.......... .-...::-+-:--=--:-:::. .. .=..: :=-.. ..:.. \n . .:.:=::=:..::....::..::: ...:.. ..+::.::.:-:--==:--== ++==:.:...... :... ...:=:-=-::.:.:::-:.-... \n .... .. ...:=+--=--=--:::---:::::::--.... ..:::::::-+X+===--==+===:-:: :...:-:::... ..::::-:-===---:::-- ::::. . \n .......:....::..::-XX+:+++=---------::=++===--:..-::.:.::-=XXXX+.++++=----.::::::+--===---:---:::::==+=======--::+:::.. .. .. \n =.....+::--:::=::+:-=+XXXXXX+=-=::-:-=:--==+X+==--::.:=:::-=+X=XXXX+X+==--=--=- -.::--==XX+==+===---+==+++:-=====--.-=-::.:.. ...-....= \n ....=.::+::---===X==-===XXXXXXXX+=X+---------=++++==++---==+=++XXXXXXXXXXX++====++==-------===+X+==+++++XXXXXX+=+=++====+=-=-=-::..-.::.... \n .... .:.---=:====+X++.+XXXXXXX+.+++++=-- -==++XX++X+XX+++XXXXXXXXXXXX+=+XX.++++=XXXXX+==--=-==++X.XXXXX XX= XXXX++.X+++ += =--=-::::::::.-..\n",
|
||||
" \n \n \n \n \n \n . \n . \n \n . . \n . . . ..: \n . .:. :: \n .: .+. :: \n .. ... .. . .. ...= .:.:: .. .:. \n .: .. .:.. ....:..::..-.:..:.. ..:::+ \n ..: .. .:. ... .. :-..::..::. . . ...:. . \n .::. :. . : . .. . ...+::--:.:----+.... . . ...:: .. \n ..-:..... .-.....-..::... .::. .:..::..::----.:.:--+-=+:.::. .. .: ...-::... ....:+.. \n . .. ..:=-:--: :-:....:----....:::.. :.:.....:-=++=-:::----:=:.. :...::..-.. . ...::- -+::::::=:.....+ \n ...........:=X=++++==-:..::::=:-+=--:-::.....+..=.:-X.++X+=-===-:--:....:.:: :-=-:-:.-::..::---------.::.:..... .. . \n ......-:::=-:.::-=+XXXXX++--::..::-:==-==+=--:::...:..:-++XXXX++++=--::-:::::.:..::-++=---+---::::--==.==-----::::+:..::. ..... \n ......-::::--==-:-=-=+XX=XX+=--=-:::::::-=:=++=--:=:.:-====+XX:X-XXXX+==----+=.--: ::::====++==-----+=+++++++==------.:----:. . ...... \n ...:..:::::-----==+===++XX XXXX+=+==--:::---=++X+X++==--=++XX X++XXXXXXXXX+++=+XXX++=--:::--==-= ++++ XXXXXXXX:+++X+ ===----=--.:::..::..: \n .....:::--===--==+XX++XXXXXXXXX+++===------==+X+++XXXX:++XXXXXX+++++++=+XXXXX+++XXX-+X+= ----=.=+XXXXXXXXXXXXXX++XXXXX+++=.---==-:--:::-... \n",
|
||||
" \n \n \n \n . \n . \n \n . . \n . \n : \n . . . \n .. . .: : \n .. . . . .. ... . . \n .. . . ....+ .. ... . . \n .. . . ... . ..::. :...:-. . . \n ... .. .. .:. :.. . .::.. ::.:::.::-. . \n ......... ..: ::..:-. .:. ... .. :-::.....::-.-:.... .: .... .. \n .:--::::..::. .::-:.=....:. ........:.:-=-:...:::::: .:.-....:. . ..... ....+.=...: \n .:.. ..:++=-=X=--:.......::=:---:::+.. ..=.:-==:-==++=:::::::. ....::::-::::.... ..:::=.::-::.... .. \n ...::-: ..:-+X+XX +==--.. ..:-==-----:-:.: .:---++++++++++=-::..:.. .......:====--:::::...::-:--:-::.::+..::..... +.. \n ..+....::--:::.:-=+XXXX++=--:.....::-=---=+--:.:....:-==+++X++=++=-::::::--:::+....-==---=--=:.:::--===+==-:-:::::::::::.. ..... \n :.. ......::+::-=--- ==+XX:XX+=----:-:.:::-==+=++=-:::+:-==+X:X+++X++X++==-==+X++==-:...:-=---=========++X-XXX+=+==----:+:::::......==.. \n ...+.::::---.::-=+XX=+X=:XXXXX+===--=--:=:-- ====X+===-=++=+XX+X++++XXXXX+X.+++++X++==-::=:-+-==++=+++XXXXXXXXX. :XX++++=-:- -==:::::::... \n .....::--=====--=+XXXXXXXXXXXX-+++++X+=---=++++-+XX++++XX++XXXX+++++++=+XX=XXX+XXXXXX++=------==+XXXXXXXXXXXXXXX+XXXXXXX+=---==----::=::... \n",
|
||||
" \n \n \n \n \n \n \n . \n . \n . \n . . \n . .. \n . . .. .. .. \n : .. ....... . ..:. \n . .-. .. .:. ....:.::. \n . . .. .. .. .. +..:.. ..-::...- . ..= \n .::....:+..: .:. ....... ...........:::::...:.:.. ..- . . ..:... \n . . .-=-:-===--:. .:+::--=.... .:-==---:------::..... .....-.+.:::. . .......... . \n ...... .=.-++==+X++=-.. .:.--:::::.:.. ..-X==-===+==-- ::.... ...-------...... :..:.::::-.+... ..... \n .+..::....:-=++++X++---.. ...--.:-:--:-: .. ..-== +X++====--:..::::........:--:::::::-:.. :::--==++-.::.......... . \n .............::-::: ==+=+XX++--::::....::--==:=--:.=...:-==++:++++====-::-=X+=-=-.-:..+--+:.:-::----=--==++XXX ==-::::::...::.. ...... \n ....::::::-=::=-===-=+:XXXXX+=-=-:::: :..::-=+=+==-.::========++.+XXX:+X++==+==X+==--....:-:---=-=--=+:++X=XXXXXX.+====+::::-::..:+:.... \n .+..::::-==----:+X++++XXXXXX+== ==++=-::--=++X++X+==++X++-++==+++++XXXXXXXX++++X.X++=--::.:::-=++++++=X=XXXXX+XXX+X++XX+--::=--::::::... \n ....:: --=====---=+++XX.X=XXX++=+++X+=====+XX XX+XXXXXXXXXXX++= ++++X+=+XXXXXXXXX+X-XX..=-- =-==XXXXXXXXXXXXXXX+.XXXX-XXX=--------:::::.-. \n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . . \n . . .. .. \n .: . . .. \n . . .:.. .. .. \n .. . .. .+... . \n .. .::..: .. ... ..............:.+. . .. . \n .:..::::---=. ..:---:. . .=--=-:.-::..=:-:. . .. ..... . . .-. \n . .:==-----+=-:. :--:.=.-..... . .-===::---:-:::::. ..::-:--:. . .:.=.::--.. . \n ..... ..:-=== ===.-:: .::::..:: .:.... ..:==-=+=+=-=-=::.+.:.... .:-:::.. ...+... .:.:::-=++::.. .=.. \n . .. ..-...:..::+====.++=-:+::.... .::::---=::.. ..:::-=+==-+X+===-:.:-+=--::=:: ..::.:. .:..:+::=:::--=+XX+---:...::.... .. \n ..:.+....:--.::-:---=+X++++==-:::::......::==++--:..:==--=-=--==+X==++=---+-=+==--::. .:::: :-::::--+=--=+XXXXX++=---:=-:.:....=.... \n .. ..::::-=-::=++==+XX.XX+X+----==.--::.:-=+X+X+===-=X+===----== XXXX++++==++++++==-:....:::.==-=--==XXX=XXXX-X:++==-++=:::--:::::....= \n ...:.-::-====-.-=++:+XXXXXX+==-==++=----- =XXXXXXXXXXXXX++=---:==+X.XX.+XXXXXX+X+XX++++-:.::--=X++:X =XXXXX++.XX++X++X+++--::--:-::::.:. \n .-..::.-=====-=-=+++XXXXXXXX+:+++XX+=+===+XXXXXXXXXXXXXXX+======++XXX++XX XX.XXXXXX=++X=--====+XXXXXX+XX+X X++++XXXXXX++=--- ---- :::.: .. \n",
|
||||
" \n \n \n \n \n \n \n \n \n . \n \n .. \n . \n . . .. \n ... : .. ......... .:.:. \n .:..+....-:. .:::-:. .:-:--=:.+.... .. . .:... \n .:==-::::-=:: .:::..... . .:==--:--::.-::.... .-.-:::. ... .::-:. \n . ..--=-------:.. ..::....:::.-. ......==--:--=-:=-+.+....... .:-:....- . .....:-==-:. \n .:......::-==--==--:..........::::=---.. ..:==--==---=-----...:=-+--:......:.... .. ........::--=++=-:.....:.. \n .....-.:::::::::-==+==--:-:.:.:... ...::-++=--:...:=---.-=---+=-==-::+==----.:-::....::..-:....::==::--=+X++=-::::+:-:.... -.. \n .......::-=-:=+=--++X++==-=-::-==::::.:.:-+XX+==---+==--=::::--+X=====--====-=+:--... ....:-=-:-::-=++++++XX++==.----==-::-:...=... \n ..:.::::------====+XX+X.++=---.==-:-::.:-=XXXXXXXX+++=- --:::--+XXXX+++.==++++++==-=-:....--++:===+XXX++XX+++X++==-===--:::::::::.... \n ....::---------==+=+X+++XX+=====+++==-:--=+XXXX+XXXX-++==--::--=+XXXXXXXXXXXXXXXXX+= =-:=:-- +XXXXXXXXXXXXX++=++++++++=--::-- :::....+ \n ....::-=====--=-++++X++++XXXXXXXXXX+++==+XXX:X+XXXXXXXXX=--.-==+XX-+XXXXXXXX+XX+XXX+++++=+++=+XXXXXXXXXXXX+++==+.++XX++=-=--=--- ::..:. . \n",
|
||||
" \n \n \n \n \n \n \n \n \n . \n \n \n .. .. \n .. . ...:. \n . .:. ...... .:=:-=-:. . . \n .:--::..-.::. ........ .. :-=-::::::.-..... ....:. .....:. \n .::--:=:::::.. ..- ...:.:. .::---:::::::--+.... .... .. .... ::-.. \n .... ...:--:-::-::.::.. .......--::.. ..:=.---:::--:::....:--:-=. . ..... .. ...:..:.:+:--- .. .. \n +...::.=..::-==--:::::::::.... .::=+=-::.....:::--.---=----:..:--::::.:.::...-+...=-.:. ..:==:. ::- ===-::....-:.. \n . .-.:-:--=--:=+X+=--=:::::---:....::-=+X+=--::.--:+:..:+:=+==---:--=--::-::-:... ..-=:::::::-===--=+++==-:::::::::::....... \n .......:: ------==++X+== ---::==-+::.:.:=+XX=X+++==--::....-:-XX+=++++========---:-:. .:-+=---=++++ +=+X+=====-------:::........ \n ... .:::-:---==:===++XX+==---+++X==-:.:-=+XX=X+XXX+==-::.:+:-=XXXXXXXXXXXXXX-++==--:....:--X+XXX-XXXX-XXX+==+====-=---::::::..-=.... \n ..::=.-------=+-===+++XX++==+X.XX+:+---=+XX:XXXXX:++:=-:::-:==+XXXXXXXXXXXX++ X+=====--===:+XXXXXXXXXXXX+==--=++X++.=--::--=:::...... \n .... ::-==--=-=+X+XX+== ++XXXXXXXXXX+++++++X++:XXXXXXX+X+==---=++=XXXXXX:XXX++ ==XXXXXXXXXXX++XXXXXXXXXXXXX++====++XX:+==-= ===--:.....:.:.\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . . \n . .....: \n . .:-:--::. . \n .::=-... .. .......... .:-::....=. . . . . \n ..:::::....... . ... . :.:-: :.::. .... . . ... . . ... ...... \n .. ..::=:::.......... .-=..:. .:::-:::.::..... .::---:. :. .:.+.:....:::. \n .-....::-=--::.:....:.:. .-.---==-.. ...:..:::::=-:.::..:--::..... =.. ...=-. ..::.....::--+-:... .. \n ..:---::--=X=--:.:.:.:-:-:.....:--=+X=::..=:::.=...:-+=---------::-::..::.......:-:....=.:----::-==-=--:......:::.. . \n .-..:--:::-=--++=-:+::::---=::...:--=+XX+=++=--::.....::+X+++=+X+=====-.-:::::. .:--:::-==-+-=:==+.+---:.::.::::.....- .. \n .......::-::------=+++==-.--=-+X+==:..:-=+XXXX++X==-::..:::-+XXX-XX+XXXX+===+-=-:.. ..:::=X++.XX++++++++++--=--::::::-:-:. ...:... \n ..:::::--::-+==-==-=+++=====++XX=++::.:-++XXX.++++==-:...:--=+XXXXXX X+XX===X+==-+--:--:-=+XXXXXXXXXX+++= ----+==--=-:::--:........ \n ..:---=---==X+++=-===+XX++==+XXX===.===+++ +XX++====+--::--=+XXXXXXXXXX++.==+X-XXX+++===-=XXXXXXXXXXXXX+=----=+X++=-----==-::........ \n ....=:--======+XX+X+=----=+.XX+.XXXXX+=+=++X+=+XX XX+==--=---=+XXXX-X=XXXX++===.=+XXXXX.X.++++XXXXXXXXXXXXX-+++=++X XX++==++===-:..........\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n .. \n :::::.. \n .... . -=...... .::..:...+ \n ..::.... .. .: .. .--:::.+... .. .. . .. . \n . ...:..-... .. .. .=.. .. .:=:::::.:. ... .:::.. .: .. . .... \n . ..:--::...:...::..: .:-:..-:... ....:::::-:....+..:-:.::. .=. .. .:....::::. \n ..:.:..::-=--:..+....::::. ...: --+-:.-. .......::+-::::: ---::::... ... ..:::. . ..::::.. ::--- -.. .. \n ..::-:::---+=-:..::..:::-:....::--=++=-:-:-:::-....:-+=-=+==++=-.-+::..:::..= ..::.....:::-:-=-----=-:......+:...= . \n .-.:::::::--.+===--:::-=:.==-:.+.::-+XX+++++-- :.. ..:-XX++++XX++++==-=-::.. ..=:=+--:++=:=-=====:=-:::...=.::......+ .. \n ......::-:--=----=++==---==-=+XX=-:..:-=+XXX+++:==--:...:--+XX:XXX+XX++=-==+-+-:=...::: +X++XX++=+++ +=+=----=:.::::-::-..... .. \n .:::::-::--+=--=---+X+===-=-=+XX++=:::-=+XXX.+++===-:-..:-=+X:X:XXXXX XX===XX-++=--+-::-=+XX++:XXXXXX++=-::--====---:::--:...+ ... \n ..:-----:-==X++==-.--+XX++==+X=X++==+==+XXXXX+++==----: :-=+XXXXXXXXXX+=====+XXXXX+++=---+XXXXXXXXXXXXX+=----=+X++==--=-=--:.+..=.... \n .+.::--==-==++XXX+=----+=+XXXXXXXXX++==+++++=XXX=XXX==------=+XXXXXXX XX.+===== +X-XXX+XX===+XXX=XX=XXX=XX+.++++XXXXX:+++++==-:.... .....-\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . ...... \n . .. .. ...=::.. \n .+.. . .:. . .:.. .=... : \n ...:.-.. .: . .:. =.. ..:.....:. . . . \n ...:::..:... ..... .:.....-: ........-+:... ..:.:::. .: .... \n ......-:--::. +.. ... . ..:::::=:...... -..:==:.:::..:=-:::. . .. ... ......: ::... . \n ..:...:-=--::...:.:+:..-::..::.---==-=-:.:.:.. ..:==----:-=+==--::.. ... .::. ...+:::---=:::-:.. .... \n ...:::=::-=--:--=::--::-==::..:.-+++===+=-::.. ..:-X++++++++++=--+-:.. ...:-:::-==------==----::.-... ..... \n .. -..::-:: ::-=------------X=-:....-+XX++====:--: .:=X =XXXXXX+==--=+-::::..:....-+====+===-= ===---::-::....+.....= \n ..+:..::::-=--::::-++==--:::--+X+=-:..:-=+XX+=------:. ..-=+XXXXXXXX+=+=--=X==:=-=::..::+++==+++++++++=--:::==---::::.::.... .. \n .:::::::--=== -:-:-=X:X==::-=++++=-=====+-XX++==-::-:..+:-=+.X+XXXXX+=-=--+XXX-X+==-:::-=XXXXXXXXXXX++=-=:::-++===-:::--::.. ..+ \n ..:---===-==+++=-.:::-++XX+==+XX++==-====+XXXXX+++=---::::-=+XXXXXXXX++=-:-==++XX+++=+=--=X:XXXXXXXX=X+++=-=-=+XXXXX+===--+:... ...= \n ...::.--+==++X+ +++=--.:=++XXXXXXXX+.+==++====++XXXXX+==+=-==+X:X-XXXXXXX+==--+==+XX.+=-=+==++XXX ++XXX-XXX: XX X-XXXXXX+ +=--+:...:......=\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . \n . .. . ........ \n .. . . .. ..:.. :. \n .... . .. .. +...-=.. .. \n ....... .. . .. .-. .. ....--:.. . ..: :... \n .. . .:::.. . : .+...::. ..:-:.........::-........:-=-::-.. . .. -... :..... \n ..=...--::-=.....=+....-:..:.....:----:.:... ..:=-:::::--=-==--:. ... ......:.:--....... \n ...:...:--=:..:::-...:::+-...:.:--=-----:::. ..:+X+==++ ++== -=-:... ...:-..---:--:.:---:::=:.. . \n ...-::..::-=:+::=::::::=-+-:....-+X+===----:.. .-+ XXXXX++X==-:-=-::..... ..-=---=---::::---- :=::.:.. ..... \n . ......:::--:.::.-+=--+:..:::-=+--::.:-=X++==----:::...:-=+XXXXX+++=-=-:-==---:.+. .:=+X=:===-.-===--::: :-:::-:........ \n ...:::.::-=---::::-=X+=--::::-==+=--==--+XX++=--=:::....:-=+XXXXX+=+---:--XX-XX+=-::..:-=++++++--XX+=--:..-:-==---:-::-:=.. . \n ..::----:-=-== -::.:=+=++=---===+= --===+XX+++===X---:..:-=+XXXXXX+==--::--=+X++==-=-::-=+XX+XX+X XXX+==-:::-=X++++=----:::.. .+. \n ...:: -----=====+=-::::-=++X++++XXX+=--==+=+:++XX:+++ ==-:--=+=XX=XXXX++==-:-==+X++==--==--+XXXX+=++XXX XX+++++XXXX ++====-.--... +.... \n ...-.:: ----:===.+==X+=-===++X.XXXXX++X+XX+=-==-==+XXXX++====+XX=X XXXX+XXX+======XXX++=-=-===++XXXX++X XXXX+XXX=XXXXX ++++===-:--:..........\n",
|
||||
" \n \n \n \n \n \n \n \n . \n . \n . ..:: \n . .. ::. . \n . . . .. .: ::. \n +... . .:.. .: .:::. .. ::...:.. . \n ::.. :.... ..::.:.. ..:-::.. .:: --:.:. . . \n =....-::.... . :-::. ..::.::-.::. .:--::.:-::-----+-.. .. .. .....:. . \n .... ::-.:....:.. . ..:=:.:..::::::..::-.. ..=+=--=+X+--=--==:... . ...... :::.. ... ::..... \n .-... ..:-=:...:::......--:=..:=++=--:::::. ..=XX++XX++===:----::.. ..-::.--:........::::-:.. . \n .....-::-...:-+:::::..-...:--::-::==++=--:.::......:-=+XX-++==-=-:::-+-::-:.:. ..:+X+==---:-::-::::.:::............ \n .. .:..---:::...:=+--::...:.:-=-::-==-=+++==-:-=::....:-=XX:X++=-=-::-:=X+=++=-:....:-=========++=-:. ...:-:::::::.:: .. \n .::-:.:::--=--::..:-=+=--:+:----=-=::-+XX.X+==---=-- ..:-+XXXXX++=--:..:--+X+==----:..:-+:+++++++=X+---:..::=X====-::.:::. \n ..:::-:::- ---+=:...:--=++=+=.==++=-:---++X+X+===-=--=:::-+XXX==XXX+=-- ::--=+:+==-:-=:-=+XXXX+=++++XX++== ==++.X+=----:::::. \n ..+...::-----------+=::--=+X+++++==:++====----===X+=+=+=+==-=XXXXXXXXXXXX+===+==+XX+==-:-=-=+++XXX+=:++XXXXXX+X-X+++++=+=---:::+:......... \n .....::::-----------=++==+:XXXX-X.XXXX+X+=-::-:--==XX+X+XX+ ++XX+:+XXXXX-X:+++++.+XXXX=--=.====+XXXX++XXXXXXXXXXXXXX+XX++==--::::--...::.....\n",
|
||||
" \n \n \n \n \n \n \n . \n .: \n . :. \n . . .. . \n . . .. \n .. ... .. .:. .::- \n :.. .:. ......: .::. ...::-:. \n ::.. .. .-. . . ..... ..:::.. ---:: =---:. .. \n . ..:::. . . ..:-.. .:..:........ .:----=+-+=----::-:. . ..... . ..:. \n .. ....::-:. ..... ..::-:.-====-:.:.... .:+X+=:++=-----:--:.. .:.:.::.... .. .:.. \n .. :.. ....--:..: .. .::..:-=--===::..::: .=.:==X+X+==--- -:.:-:...-: .:-++==--:.:..::........ . . .. \n ..::-::...:.:--:.....:. ..: ::.:-=.===-=-::--:.=.:-=+XXXX+=- -+:::-+=--+-::. .:--=-=-----=+=-:.. .=.::..- .....+. \n ...::....::--:+....:--:::::.::+::::..:-+XX+=-:-::--::..:+XXXXX=+=--::..:-=++==-::.:...:-=+=+=====++=::.....:==-:-::....... \n ....:..::::::--:....:-=---===:--==-:+::=++X++-:-:-:=--::=XXXXX++++=-:::=-:-=++=--::-=::=+XX+X+=-==+X+==-----=++==-:::::..... \n ....-::::::::::::=-..:-==X++=====-==---.::-=+=X+--- -==--:-+ ++XX+X+X+=---==-=+XX==-::-=--=++XXX+==+=+XXXXXX++++==+=--::::...:. ... \n ...-..::::----:::-:-=---=+XX-XX++++====+-:..: ---++=-+=+====+X++=++XXXXXX+===+++X=++=-::--=-==-+X++=+-+XXXXXXXXX++++ +=+=-::.:..:.-...-... \n -.....::----==-------=== +XXXXXXXXXX++-++-::.::====XX++++==+++XX ++++XX X XXXXXX++++++=--==+==-+++XXXXXXXXXXXXXXX++-XXX+++=--:::: :.:-::::.:.\n",
|
||||
" \n \n \n \n \n \n \n : \n . \n \n . . ... .. \n . ... . :. ::. \n .. .. . .... .::.:..:-.:.. \n .:... .:. . . +..=::===--::-:.. \n ..::. .::......... . . ..:--:--X+=-::::- .. \n .. ..:. . .-...:===--:.... .: ..-XX+====:-::::::.. ... :...:. \n .:.:. .::. .. . .....-==:- -:...:.:....:-+X++++=----::..-:...:.. .-=+=--:..::::... .. . \n .:::... .::. .. .. ::....--+=--::: ..-:...:=+:++X++=-+:::::-=-::.-... .---=----::-+=::.. .... .. \n .. ...::.. :: .=.::...........:=+X+=-:.:..--:::-+XXXX+===-::..:--+X=--::... ..:-+==+=---==--::.....:+:....+ .... \n ...... ..:.:.::. .:----::-=:.:=:::::.-=+X=-:. .::::..:-+=XXX+==-::.=.--:-++=--::.::.:-=++ X=----=+=--:::+-==-:::.+.. . \n ..............:-....:--=++=---==---:::::--=X=-:.:::---:::-X+++XX+==-::::==-=++==-:.:- --==+X++=---==XX++XX+==-==--=:::... . \n .-....:-:-:-::+::::-::--=+XX++=======-=::..:---+=--:==-----.+X+==+XX+X+=--===.XX:==::..:-=-===X+==-= =+XXXXXXXX+==+==+--:........ .... \n .... ::: . ------:=::--=+XXXXX-X++X==---:....:-=+++=-==-----++X+==++XXXXXXXXXXX++++=-::.:-=--==+XX+=++++XXXXXXXX++XX+:++=--:.:......:::.... \n ......::--======.----==+XXXX-XXXXX-====--:::::===+XX ++====+++X++XX++-+X=XX+XXX+:++X+=- -=.====+XX:XX+XXXXXXXXXX++++.XXX+==--::::::::::::.+.\n",
|
||||
" \n \n \n \n \n . \n . \n . \n \n .. ..+ \n . . . .::.::..:: \n . . :. ..:--=-:::.. \n .. . . ..-..:--=-:::::. \n . .. .....-. . .-::+::--=:::.:.. \n . .. .:-=--:...... .:...-=XX+=-==-:.:+:.. .. :+...... \n .. .. . ..:-:==-:::. .. :..:-=++======-:.:. ....: :--=-::.-..::. . \n .... :. ..= .....:--==--:... ..:..::-+++=====-...:::--:.. .-:=--:-=-:--:::.. \n .... ::....+:. .. ...:-+==-..... ..:..:-++++==--:...:--=X+=-::. .. ..--=---=----.:::. -....:.. \n . .. ..::==::.::=........:=++=-... .:....-+XXX+=--:....:---== =-:....:..:-+===-::-:--.:.:::::-=::+.. \n ......... .:. ..::-===-:::-=--:..:.--++--:..:.::::.:=+X++X+---:...:=--===:--..::--:-=++=---::-=X=-=++======--:..:. \n .......::.:.::...::..:-===-++=-:===---:..:.:-=+-::.--:.:::-=++==+X===--::-==++X+=-....:----=.++-::.-==XX+X++XX+=====-::....: \n ....::::::-:::-:.::--+XXXXXX++==+=--:-...:.::-=X=-::-:::::-==+===++XX++==X++X+-++-:..=.------++=======+XX.XXXX++++==--=::.:. ....:..... \n ...:.:::-==- -=.::--=+XXXXXXXX+++:---:. .::=-:-+X+=-----:=.====++++XXXX+++.X++==++=:::-= --==+XXXX.+++XXX:XXX-XX++++=++=-:- .....::::.... \n ......::--==+.==+=--=++XXXXXXXXXXX==---::: ==X+===+XX++==+++.==++++XX+=+.XX++:+==++XX+=-=+=.===++XXXXX-X.XX=XX++==-++X+XX+==.-::: ::-:::=...\n",
|
||||
" \n \n \n \n \n \n \n \n . .. ..- .... \n .::-:::. . \n . :..::-:.. \n .. .......::.::... \n .... .. ..::==--::-:.... \n .:---.-.. ...-=+:+==-::::..+.. .. .. \n . :::=-::... .=.---+==----+:..:. .. ::=-:....::.. . \n . . . . .:--=--:: .-..--=+===---:..::.::.. :::=-:.::-::.... \n . ....= . . ..-=--:. . .:-=X+==--:.. .:-=+++=-.. .:=:--:.:::::.. . . \n ..:--:=.....=. .-.== -:.. -... .-=+XX=--::. :-+===--:.. .-. ..-==--::.:=:...-.::::-::.. \n . . ...::-----.. :--. +..:====-..:..:.+..:-==+X=-:::..=.:=--==--:...::-:: +===-::..:--:: =---==--:.-.. \n ........... .=...::---==--:::=-:-:.....:-+=-:..::.....:==++.+=---::.:==-++X=-:. ..-::-:=++=-=:::-++======X= =--::..... \n +:........::::..-.:=XXX+XXX=--::--:=:. .=..:-++--..::...:---====+==+--:=:=+ +++=:. .-----=X+==----==X++++=+++==--:::::... ..... \n .=.:=::::=-:--::.:-=+XXXXX=+==--:::.. .:.:--=+X=-::::.------==+XXXX++==+ ====++-:...-=:-=--=XX++====+XXXX+++X+++==-==-:::.. ....::.... \n ....::.--==--=-----=+XXXXX-X++=+--:::::..--=X+-=+X+====--=--===++XXXXXX++=-++= =++=: :-----==++XX+=++XXXXXXX++ ++=+++++=-- ::...:::::.=. \n ......::-===+=+++===++XXXXXXXXX+++===-:::.-=+XX+==+XXX===:+X==+XX+XXXXX+XX++======++X+=---== ++-=+XXXXXXXXXXXX++==+.+XXXX+=+==--::--::::....\n",
|
||||
" \n \n \n \n \n \n \n . .... . \n ..:--::.. \n ....::.. \n .. . ..-:... \n ... ..:--:--:.::: . \n .::::.. . ...:---+=--:..... . . \n .::-+-.... ...==++=----::.... .--:.. . .. \n .-::---:.. ..:--==.---::..::=..... .=.--:..=.::=.. \n .. . .::--::. . .:-=+==--::=.. ::--:-::. .:::-:....::.. \n ..::. .. . .:-=--... .:-=X+=--::. .:-:++=--.. . .::--:::..::. ...=... \n ..:::-:.:..::-. ..:+=--:.-. .. .-==X+=-::.. .::====--:... ::.::==--::..:::....::::-=-:.. \n .. ........::-=---:.:-:::. .:-=+--:..:+.....: ==+X=-:: ....=--==+=-:=....::--+==-::..::=--:-::-=+--::.... \n .....+.::... ..:-== =-==--:..:::-:. ...:=+-::..... :-=-=.=+=--+.::-=--++X=-:. .:----+X=-:+:---++====.++==--:..:::.. . \n ........:-:::...:-+X XXXXX+--: :::.. .-.::-=X=-:..:..---:--==+X++.=----:== ++-:. .-:-:--+X++=---=++++++==++++--: :-::.. ...... \n .=..:::-:------..:-+XXXXXXX+==-::.... ..:-=+=++==---::.--:-=:=+.XXX++==-===--++-:..:-::----=+X+==-==XXXXX+=++ ++==--==::::. ...+:..... \n ....::-:-== ==-=--==+XXXXXX-+++=--::.:..:--+X+.==++=======- -=+++XXXXXXX+=======++=-::-:--.==++.X+=++XXXXXX++=+++=X+-++=-=--::..:::::... \n .....::-=-=+=++++++++XXXXXXXX++X+ +=--::--=+XX+==+XX+===.=+=++XX.++++XXXXX+====:==+X+= --==-+X==++XXXXXXXXXX++==.++:XXXX++===--.:--::::-.. \n",
|
||||
" \n \n \n \n \n ... \n ......::.. \n .:..= \n .. \n . ...... .. \n ....=..--:+:. . .. \n .:--:.. ..-=--=-:.... . . \n ..::--:. .---==--:: .. .: .:-::.. \n ..:--.: ..---=-=--::....-=.... . .:::....:.. \n .:--::. .:-===-:::=.. ..:-::-:.. ..:::- ..... \n ..- . . ..==-:... ..-=+=-:-... ..==++-::. . ::-::..-... ..... \n .:::: .. .:+=-::. .. .-=++=-:... .:---=-::+......:.:X--:..:.....= ....:--::. \n .........-::::..:+::.. ..-==::.-.. .:===++=- ... :-:--==-::... .::-+==::...::-:..-..:----:.. \n .:.=. ..:--+=-----:::....:. .::=+-.=. ::::--+=-:: ..:-::-==+-:. :.::-++= --::-+X=-=----=---:.. .... \n .-... .:::: ..:=+XX++==+=:.:..... ..:-++::::.-. .:::--=XX==-::::.:--+X-:. .:.: :-=X=--:::-=+= ==--==+--:.-..::.. ...=. \n ........::=-::..:-++XX++-+=--:.-. .:-++++-:.:--..::::--=+XXX+=--::-+--==-:. .::.::: :-=+--::-=+X+-+==:==+=-::::-::... . .... \n .+..::::-:---:-:::-+ XXXX++==+-::...=. .::-+X ===-::::-------==++X.XX+==--=---=+=:..:..::::--==+=--+XXXXX+=====++=-==----::...:.::.:. \n .:.::--=====++====XXXXXXX+==+==+=-:::::-==++==++==--=::--==++==+XXX-XX++.======X+-::.::=-=++==++++XXXXXXX+==-=+=++ ++==----::::::::.=. \n .. ..::-===+==++XX+XXXXXXXXX++=++X+X+=-=== +=++=++XX:==---=+++++++=+++XXXX++=====-+XX==-===-+X==++XXXXXXXXXX+=-==++X+X+-+===+=-----::.... \n",
|
||||
" \n \n \n .:. \n \n ...=. \n \n \n .:.... \n .-..::... \n .:---::. .:-=--:=.... .. . \n .:=::. ::.:=-- ::.-. ..::-:.. \n ..-:.. .-::=--:.... ..:... ..-... . \n ..=::. .::---::.... :--::.. ...-:... . \n .. ..+-:.. .-=--::. :--+-:... .::+:...... ... \n :..:.-. ..==:.. .. :-++==-::. .---:=-:.=. .::+-:..=. .:::... \n .:::..:.... ... .-==-. . .::-=+=--:. .:: :-=--... ..::=+--::...:........:::-:. \n .......::::+-:.::.... . ::==:..: .::--=X=-::...::.::-==-:. ...:-=+=---::-+==-:::::--::.. ... \n . ..-:.=.:-++++=--+-:-.. .:-+-:...... ...::-=X+=--:...:.:=+=-:. ...:.::==-:::+-==:----::--=-:. ...... .. \n . . ...:: =...-==+X++===-:-.... .:=++=:...::. ..:---=+XX+=-::..::--==:. .:... ...:=-::.::=+=====--==+-:.....:+.. ..+.. \n .......::::::..:-=+XX+=.=-==:::... ..:-==+=-:.....:..:---==++XXX=-:---::-+=::. . ..=:+:-.-::.=+XXXX====-=+=-:-:::::.. ...... \n ..:::------=--- =+X:X++==-== =++=:.:.:::--=:===-::....:+:-= ==++XXXX+==--===.X+-:.. ...:-=---=--=+XXX.XX+=--====+=+=----:.......... \n . .=.:------==+X++++XXXX++.==-==+XX+-+-=-===-=.=+++==::::-=X++++=++XXXXXX+=--==-=+X=-::--:-=X===+=++XXXXX=X+=-=+=+XX+=====-=--:::::=... \n . . ...:--======+++XXXXXXXXXX++++++XXX++===:+++==+XXX.++=---=+XXXXX+-+++XXXXX+=======+X+====++=+.++XXXXXXXXXXXX==+++XXXX++ ======---:.::..- .\n",
|
||||
" . \n .. \n \n \n \n \n . \n .... \n . ..--....... . \n ..:==+::. .::--+.. . . .. \n .+.-.+. ...:--.:. .. =..-:... \n ..-:. .:-:---.. . . . .:... \n :+-. ..:--+... .:-::.. ..:-:. \n .==:.. ..-+--:... .::-=-.. .:=-.. \n .:-.... .-+-:. ..:++--::.:. .::----:. ..==:::. ....: \n .::...... .--=:. .:-=+--::.. .....::--:.. ..-+-::.. .. .. ...+::.. \n .. ...:-::.... . .:-:.. . .. ..:-=XX-::. ......:=--.. ..:-=:-:..:==-:::.=.-:::.. \n +..:..:-=:=+--:::.... .:==:... .. ...--=X+--:.. ..:==--:. ..-:..::--=-- ::-:-::-:. . \n .......:--=+==-:-:::... .:--==-.. . ..:--=+XX+=-::...:-=+-:. .. . ..:-::..:--==-==::.:=-:... ..=. \n .........:....::=:=X+=-----:--:.:. ..:-=.=:.-. -...:--==+XXX=-::--::+=+-:. .:::+:...:-=X++X=----==-:::::..-. ..+. \n .+..:::::-::::.:-=+X+==-:----+==+=:+..::=--==---::. ..:-=:--==:++XX-=-:--==-=X=:. .:--::-:::-=+=XXX++=--=======- :::....=:... \n .. :--:-:--==+++=+X+X+==-- :--=+XX=-:---=--= ====--...::-+==+XX++XXX:X=--::- -=+=:..:-:::-==--=-==+XXX:++=+-===+XX==.-----::::=...: \n . ..+:----=--==+++=XXXXX+==++===-+++=---=+==-+XXXXX+=-::--==+ XX+XXXXXXX++=---=--=++-::--==-=X=:+++XXXXXXXX++-== +X-X++=---- -:::+::.. \n . ..:::--=++= =+=+=XX+XXX:X++XX++++++==++X++==+XXXX-X++=-==++XXXX:X X-XXXXXX+=+===+-++==+-===+X+X:XXXXXXXX.X+===+XX++XXX+==.--=--:::+:+....\n",
|
||||
" \n \n \n \n \n \n . \n .-:. .:----:.. \n .:--:=.. ...::=-:.. .:. \n . ::.. .:--::. ..... \n .-=:. ..:--::. . ..::. \n :=:.. ..==::. .:--::. .:-:.. \n ... .-=:. :..++-:.. . .:.:-::. .-:.. \n ... :=:. ..=+-::... . ...::--: .--:+.. .. \n ...=... .::. ..--+==-.. .:. ..::-::. :-::.. . . .... \n .. ::-.. . .::. .:-=++-:.. ..==::. .::... :--::.....::.-.. \n . ...+:::--:::..= ..---.. ..:-=++-::. ..==-:.. ..::. .=::-:::::-.. ::.. \n . . ..::::----.::::.-. . .- --:. ..:--=++=-:....+.-=+-.. ...:. ..::-----+-:.:-:: .... \n -...... ..:---==-::.::-:::.::. +..:==--:.. .:=::--=+++X--:::::::=+-.. .:..:.-...:=+===+=-:.--:::...... \n ...::..:.:: ---=++=-::.::-++--:==:..::-:-+--:-.. ..:-::--++++ +=:..::--:=+-.. . ..:-..:::::-+X=++=--::-==-==-:=........=:. \n ..:::::::--:-=-++X+=--:::::--=====-::-= --=--=-:. ..:-=---=+.++XX+=::...-::-=:. ..:::.:-::=::--=+XX+====-=--=++=-+::::.. ....- \n ..:=------:-=-=+X+X+=--=-----=.=--:--X--=+X+++==-:..::-==++XXXXXXXX+=-::::-:--=-.. .:-:--+--=-=++X+XX+++==---=++==---=:::=:::...+ \n +..::--=---===+==+++++++=-++========-=XX+==+XXXXXX+=:::--=+XXXXXXXXXXXXX+-----====-::-:-==+XXX-XXXXX.XX X+=---==+++-=------:::::..=. \n ..: :..:::-==.=--=+X++++XX+-XXXXXX+:==++++XX+++=+XXXXXXXX=--=+XX-XXXX XXX.XX+++ ++==+XX++==-=-=+ X-XXXX+XXXXXX+==.++=++XXX=----=--::=:::: ...\n",
|
||||
" \n \n \n \n \n .. ...... . \n .::.. ..::=+-::. \n .--... .. ...-:.. ..- \n ..:-. ..-:.+. .:. \n .-..- ..=-:. .. ..: \n -:. ..+-:.. .:---::. .:.. \n ..- ::. ..-+:::. .:=::. :... \n .-. ..-+=--:. . .+.-:.. ::.. \n . .. .:. .--====:.. ..=-:. .:+.. ... \n ...... ..-. .:-==-: : .+=:.. ... ::- .....:.... . \n ... ..... . ..:::. .:.:-===-:. ...-+-:. .. .:.:-:....:...:.. \n .......::........ .:=-:: .::.::==++=:=..:..--=-. ... . .::-:: :--:.:-.:. .. \n . . ..::::---:. ..:.:....--:.....::--:--: ..:+-:-:-==++=::...:::-=:. . ........:-+-:-=-:::::...=.. \n -...-......-::--+=-:....:---.-=--=:+:-=--:-::.. . .:-::::=====+-:. .: :--. .. . . ....+..-++===--::::--::-:..+. \n .-.::...:...::-=+=-::.-.:.:---.=--::-+-:---:-:.. . .:-=--== == ==:.. ..:--. ..:..:......:-+++===--::::-=-:-:... . :.. \n .::::::::.::--==+=--:::+ :::-----::-++==++=---:......:-+X++X+++X++=-:......--:. ..::=-:.:--=++++==--=: :-==---:::......... \n ..:::-::+:--=--====+===+==--------:-=X++ XX+++X++=-:..:-=+ XXXXXXX X++-:::: -.--:..:.:-=+X+++ +=+XX+X++==:::--= ==--.:::::..... . \n ....::---.::-++===.+XX:XXXX+++--=====+++==++.XXXX++==-::-==+XXXXXXXXXXXX+=.==-==+==+--:--=+XXXXX-XX.XXXX=+=--==-==++=-:.:--:: ..:.:... \n .+...::-:--===--=++=+++++XXXXXXXXX+===+X+++++=+=+XX=XX++====++XXXXXXXXXXXX=++==+=+XX-XX++=-:==++XXXXXXXXXXXXXX+==+X=++XX+=-:-=-:::: :::=..:.\n",
|
||||
" \n \n \n \n ... ..+-:.... \n ::. ...::-::.. \n .::. . ..-... . \n .. .-=:: .. \n : .:=-:.. ... .. \n :. .-+:::. .::-=::. . \n . .:X-::-. .. ...:... . \n .. .:=--:-.. ..-:. . \n . ::=-=--.. .+-: . . \n .... ...:---=-:.. . .-=:.. . ..::. .. \n . ..:... .-:..:--==-.. ..+.:+-:. ... ...::.. ..... \n ..... . .:::=-:. .::...-=-==:. ..::-=:. ... .:.::.:..:...:. \n ....--:..... .. ..-:.. :=::.::. . :--:.:----=-:. ...-:. . . . ..-::.:.::..::-.. . \n .. ..::-::. ........-:-=::==::..:..: .:--::--=----.. ..::. .. . .-=----::....::.::.. \n .... .. .:::-=-:.. ...::-:-=-:-::=-:=::.. .:-=---=====-. . .-.. . .. ..-==---:.:-....::::... \n ...........::--=-:::..-::.:-::--:.:-+==--:..-... .:-++XX++++==-:. . .::.. ..:.=. ...:-=++- -::::..:--:-:=.. . . . \n ....::.:..:-::--=:=----:+:-=--:-:.:-++XXX+=-=+=---:...:-=+XXXX=XX+==:....+:::::. ....:=+==-::--==+XX==--:..::----::. ......=.. \n ....:-:::::+=----==++X++====+-:----.==+=+++X++=+==--=:.:--=+X=XXX=XXX++-:+::--=-:--:..:-+XXX+:++==+XX XX=-:-.----+=-::..::......... \n .. .. :::----:::-=--= =+XXXXXXXXX+=--===+-====++XX++=+=-----==+++XXXXXXXX+++==+=XXXX++=-::--=+X+X++X++XXXXXXX=-==+===++=-::::::::...::.... \n ......::-===+=-=-====++-+XXXXXXX..++==+X+======+XXX-+=+=--=+X++++XXXXXXXX++======XXXXXX++-===+++=XXXXXX=XXXXX++==-++XX+XX+= ---::::::::::. .\n",
|
||||
" \n \n .. :+:. \n ... .--::. . \n . ..--:. \n .. .--:. \n .. .:=:+.. \n . .--...:. .-:.. \n :=:..:... .::-:. . \n .=:. .. . :-:. \n .::::::. .:=:. \n .. ::-::::--: .. .--.. .. \n .. .-. .:..::+:--.. . .:+:. . ..... \n . .:...: .. :.:=::: ..:=:. . .... . .. \n .. .. .--::.:. :.:. ::.:::.. .::. ..:.. ..... \n .:...+ ::...:=-.....:. ..-=..::-:=::. ..: .:-....... .-... \n . ..:.. ..:-:::..-:...=.. .:--:::::=-:. :. .-=::::... .:... .. \n .. ....:::. .+.:+:::-.:-::-:. .. .:-=--:::--.. ... .. .-=-:-:.... ........ \n ..... ..:..::---:+.. ..:==:::- ..:==-::.. .-.:. ..:-=+=====--:. . ..:.. ..:. :--++-::.... .::..- \n ..:..- :...::---=-::...:-+:::-:..:-++X++-::--:-::. ..-+XXXXXXXX+=-. . ....::. .:--=-:..::--=++-:::....:-:-:.. . \n :..::......:.::-=X++==--:-++-::-::---=+++X==-----:-:..:-++X=XXXXX:+=-.-..:::::::.....=+XX+=---=--=+X++=-:: :-:-=-:.. .. ..+. \n ..+::-::..:::=:--=+XXXXX+=+X=-:--:=:---==+XX+===--::-----==XXXXXXXXX+=-+-=+X++++--:..:=-++X+=====.=+XXXX+=---=--==-:....: ........ \n .. ..:=---==::-:--====+XXXXXXXX+==-=- ==+---=+XX.+==--:=--===++XXXXXXXXX+==-==+XXXXX+=--:--.++XX++++.X+XXXXX+=--=++++++=-:.:::::...+:..-. \n ....::--=++=+=---.==++.+XXXXXXXXX++=+==+-====:XXXX+++=--====+XX-XX:XXXXX+====:==+=XXXXX+==-=++XXX-X==XXXXXXXX++==+X=XXX++=-:::::::::.::...-\n",
|
||||
" \n . .:: \n .+. .-::. \n .:--:. . \n ..--.. \n . .:-:.. \n .-:...::. .:. \n :-....... .--:.. \n .-.. .. ..:-.. \n .:+:=.. .:-:. \n . ::+::..:=: .-:.. \n . . ..:.:::-. :-.. . .:.. \n .:..-. . ..:.... .:.-. .... \n .. .:-:... . .. :.::... .-:. ..... . \n .. -.. ..-::.... ..:-..-.::+.. .:. .::.. . .. \n .. ..:..:..-::.. .. +:::.=.:.:::. . .--:. .... ... \n . +.... ....:: :-:.:..-:=.. .::-:::..:-:. :. .-=::::... .. .. \n .. ..:::=.. ..-=:::::.:.:--... . .:--------::. ..:. .. .--=--:. ....-. \n . .. ..+..=:::.... ..:+-:-::.+.:=+=--:. .:.:.. ..:=+++++++=--. ..... :...:. ..:-=+-::.-.. .:::.. \n ..::.. ....:-==--::.=.:-=-=:::..:-=++XX=---=:-::. .:-+X+XX.XXX+=-.:. ....... . .-++==:..=::--==--::..:..::-:.. \n ..-.::... ..::::-=XX X+=---+=-::::-- -=+++X++==---::-:::-++:XXXXXX-+==:..:-=--:-:.. .:==+X+=------== +++=:::--::--:.. .... ..-. \n . ...:::-:...::-- ==+X-XXX:+++=-:---.-::-==+X+X+=---: ----==+ XXXXXXX:==---+XXXX-X=-:..:-++ X+====-=++XXX+X=::--= =-=-:.............. \n .. .:.--====--::---==-++XXXXXXX+==-=--==----+XXXX+==--:==--=++XX.XXX-XX++=---=+XXXX-++=-:--+++X+++ ++XX=XXX++=--+XXX+++=-:.....::.+.:..+. \n ......:--==+===-:--=++XX+XXXXXXXXX++++=====++== XXX-++=---===++XX+:XXXXXX+=======+XXXX++=====+XXX++XXXXXXXXXXX++==+XXXXX++=-::::::-:::.....=\n",
|
||||
" ::.. \n .-:. \n .::. \n ..: .. \n : .:.: .. \n :.. . .::. \n :. .--.. \n .::. . .::. \n . :. . .. .:. \n -...:...:: :.. \n ..: . .:. .... \n .:-::. . . ..-. .:. . \n .::. .. ..:.. .. .. \n ... .:.... ....:....... .. :.. \n .:..:...: .:. ..=:.. ..... . .:-...+. \n .. .:=::::... .:. .::::. ..... .. . .=:.. \n .=... .-=:....... ::.. . .::::::--:.. .. .:-=-:.. ... \n . ...::.+. .-::......::-.--:.......... .-==+==--=+=:. .... ..:--:.. . :..:. \n .. ..::-+=-:.. ..=-::-...-==+==+-:--::-.....:++XX+:+ XX=-:. . .:==-=:. ...::---.. ..:...::. \n ..:.. ..+:-+XX++=::::-=-:....:---==++===--:::::::-=+XXXXXXXX+=-:... :=:-.. . .--==+=-::..:::-===+=:.:::::+-:. ... \n . ....::.. .:.::=++XX++==-==- ::-::--::==++=+=--:-=: ::--=XX=XX-XX++=--:-XX+===+-....:=+=++--::----=++=.++::::---:-:. ..... \n . ..::-----:..-::--==+X-X+++==--::--:-::-:-+X+=+=--::-=:::-==XX X-XXX++=+--=+XX+ ++=--..:-=++ ==--==+XXX++-+=--+++++==-:.............. \n ....::--====--::==++XXXXXXXXX++==-=--::-=++=X++=+.==-::-:--==+X+XXX:XX+===--=++XXXX++==----+XX+====+XXXXX=X++= =+XX+XX+=-:..:::::::..-.. \n .....+::--======--= XXXXXX.X-XX+XXXX+=---===+XX+=+XX.++=--===-++XXXXXX=X.-++====++XXX.X++---=++X=X=++XXXXX-XX.XX++++XXXXX+==-::===-- :::.....\n",
|
||||
" -.. \n .. . \n .. .. \n . ... \n . .:: \n .:. :. \n ..: .. \n ... : \n .:.. .: .. \n ..:.. .. . \n .:::.. . . . \n . .. .. . :. \n .. ... .. . .. .. . :. \n .:-:... .. :.. . .-. \n .-::.... ...-..:=:. .-:. \n . ..::.. . . ..-.. .. ..:....::-=:. .::.+.. . \n ..:-:. .-:. . .::=:--:..::.+.....:-=-----::-=:.. =.. ..::.. . . \n .. .:-++=-.. .-:.. ::-.-:-::=-:......:=+X:X+===+X+-:. .::--. . .:::-:.........: \n ... ...:=XX+==-:.:..-::.. .:--------=-:::...::-+XXXX++++X+--... ... .-----:......-: --+=:.......:. . \n .:... ..::-=+++=--:.:-::..:::::--==+=---:::-..=:=:=++XXX+.++==--::=+==-::.:. .:-+-=--:..:::.-==-==+=::.:::.:. . \n ....:::::....:::--=+X++====-::..::...:-:-+X=--=:-:-==:::--=+XXXX+X+==--:-++XX====-:....-==+=-:::--++++====-====-==--:. . ....... \n ..::::-:--:::=X=== =+X++:+++--:.::.-.:-==+-+=-==-::.::::--=+X-XX++++==--:-=++XX+====-:.:-=XX=--::-=+XXX++===.=XX+==+=-:..:-::.....-. \n ......::-----==--+X+=+X:XXX+++XXX==--:::.-==+XX++==++.-:: -- ==+X+XX+XX++=+=-==++.XX+=+=-:--=+++=--==+XXXXXXXX+===++X+++==:::-=-:::: :.... \n ...+..:::-=+=----===+++XXXX X+XXX+X+=---- ++=++X++XXXXX+=====+X+++++XX+XXX+++===+XXXXX+==---=+.++X++XXXXXXXXXXXX++++XXX++=--------::-- ::....\n",
|
||||
" \n . \n . .-. \n . \n . . \n .. . \n .. . \n .:. . \n .:.... \n .. \n . . . \n .:::.-.. . .. \n .:.. .:.. . \n ...... . . ....::. .:. \n .. .:. . .:..= ..... ...:.+..:=+. . ... \n .::-:. ..: .:..=:..:--:......::-:::-+:::-:.. ..- ..= \n ..--=--. . . .::..:..::.. . .:=++XX+==-=-=:. ..... :..-:. . \n .:==== -::.. ....-. .::.:-::.+::.-....:-=++XX=====--:. .. .::..: . .:::---:. . \n . . . .:=====-::.::++......:--+-=-:.:::......::=++XX+=+==--::.:==--:. ..-.:-::. . ..:--------:..+:... \n .. .........-:.:::-====-=:--:.. .....:::-==-:.::::::::::--++XX+==-=+--::==+XX=--::.. .:--==-:+..::--==------=-::--:.. .. \n ......::::..--=-:---=+==-===-:.. .. ..:====+-::-::..=:+:::-+XXX===+==-::-=+++X+=---::..--=+=-:...:-=.+++===-=+++=--=-:..:::.-.. .. \n ...:::::---:-==--=++X+-++=+==-::.:..::-=+X++=-=--=-..::-::-=+XX+=++=+=--:-=++XX+=-=-::-:=+==-::::-=+XXXXX+===++X+=:=---::-::...:.... \n ......::-.---:----==++XXX XXX++:++=--:::----=++XX.++++=:::-:-==+==+XX+XX++-=-==+XX++===-::=== -======+XXXXXXXXX+==+XX++=---=::-::=-::::=.. \n .. ..::-=----::-:===++.XXXX-XXX=XXXX==--.====+=++XXX.X++====+.++++ +X++XXX:+=.=++XX+:++==--==+ +:XX-XX-XXXXXX:XX++XXXX++=- ::-:-::::::::..=.\n",
|
||||
" \n \n . \n \n \n .: \n \n ... \n . \n -.... \n ::.. \n .. .... . \n . .. . ...:--: \n . . .. ..-.::.. ......:::-: .. \n .::=. .. . .::.. -...:-:--::..... . . ..: \n .::=:--:. . ...... ... .:-===+=:: :::. . . ....::. \n .::::::.. ... .. ...::=. .:. .:----+++--=::-.. ... . ...::::.. . \n . .:------....... . .::::-:...:. .:-==++==------::..-.--:. ...:::. . .:::::--:. ..-. \n .. :....:.:-----:.... .:-::--:.:. ... . .::-=+X+=------::+=+==+-::. .::--:.. ..:::-:::-:-:+..... \n ......:-..:::::::---=---=---:. . .:=+-=-:::....-.:.:::-+X+=----- -::-++=++--:::.+..::-.-:. .::-==++-=-====-::-::...... \n ...:...+::+::::::-+++ +=----::.......::-=+==-=-::-:. ...::-+++=- -=+=-:::-==++=-::-::=:.-=--::...:-++XXX++=-==++=--:::............ \n .. .+.:::::.::+----=XXXXXX++======:::..:--+-++.++-==--:.:---:-===++==++.=----=+X+==+--::-=-----------+XXXX:XX+====+-+=--::.:::..::::..= \n ......::---::.::-- -=+X=XXXX+:++++XX+-::-=====++XXXXX+=----==:====+XXX:XX X==-=++X==-+=-::=--=-=+X++++XX-XXX:XX+==+XX+==-::::::::::::::.. \n ......::--==---::-==+.+++XXXXXXXXXXXXX+==++X+ ==++XXXXXX+-==.+++XX ++XX+XXXXX++=+=+X++X+=-: --=++XXXXXXXXXXXXXXX+-+XXXXX+=--::-.-::-::::.....\n",
|
||||
" \n \n \n . \n . \n \n \n .. \n ..... \n . \n .=. \n . .. ..::: \n .:.. . .::. .... .: . \n .... . .. .:::::.. .. . \n . ..... . . . .-::---:..+. .... \n ......... . .:+: . ..-::-----:::... .. . . ..+.:. \n .....:.. ...::...+ .::--=+=-:::::-:...:::-: ::. ...::-:.. . \n ....:::::::- . . .:=:.::.... .::-=X+--:-::-=-==:--:-:.. ...::.. ..:-:::::...... \n :.. .. -...:.:---::::.::. ..----:::: ........:-=X=-::::.::.:==-----:.+. .:-::. ...: ------.--:.::. . \n ...:...:.....:-=+X+=--::.:.. .- -=-.-:..-::. . ..:=== --::-=-:..:-==+=-:..::-...=::::.. ..-====+==-----=-:::::... . . \n .......::.::.::=++XX-X+=-+--=-::. ..:-=-=+===--:::.. ::-:-=====- ===-:.::-+X=-- ::::-=:::--.:..:-+XXXXXXX+= ===--::::............. \n .....:: :::..--::-==++XX:X====+++==:.=.:-==.=+XXX+=--::::-----==+XXXX-++=-::=++=.---::..::.--=++=--=+XXXXXXX++===++=--::.::........... \n ....:::----::-:--.===-++XXX++X++X+X++-:-=++:===+XX-X+==--===++++++XXXXXXXX+=--==+==++=-:.-::==+XX.-XX:XXXXXXX++=++X++X+-:::::::::::..+... \n .....::---===---=+XX:+++X+++XXXXXXXX++===+XX+-==+XXXXXX=+++XXX:XX.X+XX+:XXX++=++==+XXX+=--:--=+++XX:XX-+XXXXX:++=+XXX+XX+=-:-===-:::::.=....\n",
|
||||
" \n \n \n \n \n .. \n \n \n \n :. .. \n :. . .... \n . . .... . \n .. ...:.. . . \n . .. ....=::.. . .. \n . .. . .:-:-:....... :.. .. ..:. \n ..: ... .:..... .::+:-=-::..:::+.....=:. .. .=.:. \n ...::.+... ..--:.. .. .::--=-::....::=-:--:.:. ... -.....:..-. .. \n .. ..:-=--:: .. .. ...--::+:. .. ..:-=--:.. ..:.:--::-::. . ..::.. ....::..::..-.... \n ..........:+==--.:.:..:. .:.:-+-::.. ... .:--=--+:::::. .:--+=-:...:. ..::.. ..::--==--::::::..... \n :......:..:...:-+=:==+--:::+=:.. ...-==+==-:.-.. ...:---===---=-:-..:-=+-:...=.:::::::::....-=+X++++.+=--+-::...... .. \n :...+....::-::-=====:++=======+-:. ..:-==+.XX++-:....:+::--==+XX++-=-::.:==+--::+......:--++=-::-++XXX.++X ==+=--::..::=:+....... . \n ...+.::--:::+===+= ==+++X+=+.+==++=:.::-====+XX++==--::--==++=+XX.XX=XX+-::--==-==--.....:-=XXX +=++XXXX.+X+++++=== =::.::-::........ \n ....::+:-=----=+ ====+:++=+++:++++= =---+X+==++XX++====-=+XXXXX+XXX.X-XXX++--+==+X++=-:..::-++XX++XX+XXXXXX++==++++=+X=--:-:-::..:...... \n . ....::--=+++==+X=++=+===+++XXXXX++:==+++++XX++XXXX+++++-+X.XXXXXXXX+-XXX:++++X+=XXXX-+=-:--=++ XXXXXXXXXXXX++=-+++XXXXXX=-==---+::...:. ..\n",
|
||||
" \n \n \n \n \n \n \n . . \n .. \n .. \n .. \n . . . \n . . .:.. . \n .. . .--:...:. .. =.. .. \n . .. .::... . ..-::....-: .. . . . \n .:-::...: --::. ..-:-:.... .:.:...=. . . -.... \n .--:::::.. .:--:. .::-:.::.. .....:-::-:. . .. ........ \n . ..:- --:--....=. .:-+-:.. .::--:-:..... =:::-:.. . .. =..:-=-:.:..... \n .. ...... -----=--:::--.. .:-=+++-:.. ..:-----+::-:. .::--:.. ......:::.. .:=:===--=+-::::.. ... \n .+.. :...:--:::---+==+=--==--=-. ..:-=+XX+==:. .::.-=-=XX++=--:.. .: -- ::.. .+.:-+X=-:.:--=++-===+==-=--:..+...::.. \n ......::..::=-------====:-+===-==-.. .:---=+X+==-::::: --==X=XX XXXXX=-:.:------:::. ..::+X+ ==--=++X++=++==+=--:--:..:::..... . \n .....::=--::=------==-= ====++==:--:::=+===++X+==--:-::=++XX -:XXXXXXX+-::---=++=-:.. .:-=++==+===+XXXXX+===++=--=+=-::-::.+.....: \n ....::- ===--======-+-===++XXXX+=---+====+X++:XX++++=--=+XXXXXXXXXXXXXX++==+X++XXXX+-::.:--==+X++++=XXXX.++=====++ XX++=:--::::..+.. .. .\n ....:::-:==++==== ==+-===+=+XXXXX+==-= =+==XXXXXXXX++XX===+XXXXXXXXXXXXXXX+==+++-XXXXX =--===++XXX=X.X.XXXXX++:==++XX+XX+=.--:::.:.........\n",
|
||||
" \n \n \n \n \n \n \n \n . \n \n \n ..- \n .. .::. . . \n .:. .::.......... \n .:... -:..+ . .:...=. .. \n .::..:.. .:-:. ..=::.:... ..........: . \n ..: -:::::....-. .:--:.. ..::---:..... ::..-:. . .::...:. \n ....-::--:-=:::.::. ..:.==::. .::----::: .. .:.::..: .... .::--:::-:... . \n ...::::..::=-=---.--:--:. .:--XX+=-:.. ..::-=====--:=:. ..:-::.+. ...:==-:-..:- -=----==-:::...-.. .. \n . ......::-:::::::- -=---.---=-:. ..:--+X+==--::. ..::-=+X+XXXXX+=:....:-----:.. ..:-XX==-::---====-=+=---:::......:... \n .-....:-::-:--:--: --==---.--==-- .. :-=--==++=--::::::--==+X XXXXX-X+-:::=--:=-:::. ..:-=+=--=--==+++==++==+=-+--=-:.::..... \n ....:::---:-----=--=====-===-+==----+--=++==+X====-::--=++XXXXXXXXXXXX+=+--==+XX+=-:....:-==+====+=+XX+X+==+==-==.===-::::::.:.... \n ...+:---===---.======-===+X:XX++==-----==+XXXXXX+==----==+XXXXXXXXXXXXX++=+X+ +XXXX+=-:+:--=+++++X++XXXXX+++=====+X++.==-::-::......... .\n .....::-==++.==-==-++=+==+++XXXXXX+========XXXXXXXX-+++++XX-X:XXX+XXXXXX ++=++==+XXXX:X+==.=++XXXXXXXXXXXXXXX+===:+XX=XX+=--:-::::.........\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . .. \n . .. \n . ..+ \n .. ..:.. ....... \n ....-..- . :-:. . ...::.=. .. ... \n ...=..+.::. .::. : :.. ..::-::....... . ..: .... \n .. ........:::::..::. ..:-=-:.. ..:-----::..-... .... :... .:.:.....:. .. \n ........:.+.::.::.:-:. ...:=+==-:..- ..:-=+X+=--::..+ . ....:. .:.:=-:.. .:..::::::-:. ... . \n ............::::::--::-=-.. ..:-=++=--:::... ..::-=+:XX+-+=-:.....::-::.. .:==-:-::+::::----.-=-::..:.....+. \n .....:..::::::::::--:+::::--.=:..-:-:-==:=+=-:..+.:.::-==+XXX++XX+-:..::-==--:.. ..:-==--:::-=-==-=--=--+-::::::.... . \n ......: ::.::::.---.-=---.=-+-----:::::--++====--:..-::-==++XXXXXXXX+=--::-=+XX+=-:.....:--=------==X==:==--=-==-----:......... \n .:.:::.---::.--+:--+-==++======-:--::---=+X-+++==-.::-=++++:XXX:XXXXX+======++XXX+=-::.::-========++X++++==+====++==--::... -..... \n .....:--:===+------=--====+XX.++X+=-==---=+XXXXXXX+==----=++:X:X=XXXXXXX+=++===+X.X++==---=++XX+++ XXXX==XX++=-==+XXX++=-::::::.......... \n ......::-=.=+ ==--=++++====+X=XX:XXX+ +X==++++X XXXXX++===-+XXX.XXXXX.-XX++=+==.=+XX-+++:+=++XXXXXX.XX.XXXXX+.X+=+++XXX X+=--::--::.........\n",
|
||||
" \n \n \n \n \n \n \n \n .. \n \n \n \n .: .-.. \n . .... .. ::.. \n ..... :.. .::. ...::::......... \n . ...:.. ..:. ..= ::.. ....:--==-- ::.. ... ... . .. \n ... .......::. ..:----::=..= ..:-+XX= =--:... ..=.::. .=.:.:.. .. ..=...::. \n . +......:.........:: . ..:-----=::.. ..:--=XX+====::.=....::::. ..:--::=....-....:::::::.... .. \n .... .. .=.::-::::::....:::..=.::-------::....:.:---=++===++==-:..:-==--::.. .:----::::::::--::--+:::::....=.. \n ...............::----- -:: ::::.:=:::-===----:.=..::-=-=+X+X++++==-:::=-+XX+=--. ...:::--:::=::-=--=---:----.::.::. . \n ....::::....::::+:---:==---=-=-::+::::--=X+===-:+:..::-==++X XXXXX+==:--=-=+X+=+=-::..::------:--==+=++==---====---::.-.... \n ...:::--:-:::::------===++====++-----++-=+X+X+=:=-:.:::-=+-+XXXXXXX++++-==-=+-+===---:: -=+++== =:+++XX+++===+=+X+==--::... .-... \n ..+...:----------=========XX=X++-+X+===---==++=XX+==+==--==+++XXXXXXXXX+++==--==+X++===---++XXXX+.++XXXXXXXXX++===+XX++==--:::::.-.......- \n ....=..::-========-=:++++ +++XXXXXXXX++=====+-=++XXXXX+++++XX++XX:XXXXXX.X+======+XX.+++++++==+X:XXXXXXXX+X:XXX-X+++XXXX++==--:--:::::.......\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . \n .. ..... \n . .::..:. \n .. .:.. .. ..-:-:::.. .. \n . .-. ....... . ..:::=---::::.. ... . ... \n ... . ... .:::...... .. ..:--++=-----:. ...:.. .. ...... . . \n :..: ..:. ... ..:=--::.... . .:: -=++==-=---:=....:::... .:::..... . +....... \n ..:-:.::.. .. ..-...:-:-- :::.....::---+====- ----::..:--=:-:.. .:--::.......+=:.:::........ \n . . ..:-:::::+.....+=.::-...-=--::::....:.::- -+=====+=+-::+:-=-++=--:. ..:-::.:.. ..::=::-::=-:::::..... \n ...............::------+::::::..::::::-=+==-::... .::--=+X+++ +X+=-: :--++==---::...::=--::.=.::--====--:--==-::.... \n ..:-::::..-.::-:-=-- =+==--:--=-:::::---=++=-::::....::-=++XXXX++++=+=-.--===.---: ::.:-=++=--:-==+++++-==-=-=+==--::.......... \n ...:::.:::::-:----- ==+XX+=====++---.:--=++++=---=-+::---=+XXXXXXXX+==--:---=++===- ::---+X++=-===++XXXX++=-==+++==--::::.:...... .:. \n ....:.::----------===-===+XXX X++ +X+===--+-==:+XX+==:====++=+++XX.XXXXX+===--==+XX+= ==--===+XX+++++X=XX XXXXX+++++X++==-.-:+:::.......... \n ..=...::---.-==+=+==++++++++-=XXXXXX X++++:+==:=++XXX++ +XXXX+XXXXXXX+.+XX++===+XXXX=X+.==--:==+XXXXXXXXXXXXXXXXX+++XX=++++-=-----.::........\n",
|
||||
" \n \n \n \n \n \n \n \n \n :... \n .:..: \n .. .:.:..+. \n .. . .. ...::.:.. . .. \n . .:-:-:--:...:. . . \n .. . .:..... . .::+===--:.::-:=.. ...:.. . \n .. . .:=-:::.. -.. .::-+===--::---:.....::+.. .:.... . ... \n .:...... ...::--::-:..... ..::==-=--::-:-.::.::--:::... .--::.. . .... . \n .::::........ ......----... .. .:+::==-------=-::::-===-::::. .::--:. .. ......::........ \n .. ..+.::-::--:..:.:...+..:..::==-:....: .::::-++========::.:-+=.=-::::.-..:-.-:::. ...::-----:: ---::.. \n .....-.+..:.:.:::--+==--::::-:-:..:::::-=--:+...+...:+:-=++X++== =--:::-+--=-:::..:..:-==--::.:--=+++==-----==--::.. . \n .....-.=..:::-:::--=-=+=-----:-=::.: ::--==--:::::..::+--+XXXX-====--:.::-=-==---::.::::=+==-----=+XX++-==:-===.--::........ \n ....-:::::::.------= -=+X++===+=.==-- --:--==++=-+--::-:====+XXXX+-+==-::=--=++==---:.:--==++==--=++ XXXXX++===+++===-::-:::....+:... \n ....-.::-:------====+=.=++:XX+X++++ ===+ ======+XX+==-== ++++++X+XXXXX=+==-==++XXXX==--::+--==XX++-+X XXXXXX:++++++++====-+-::::::........ \n ...=.::--=-=== ===++XXX++XXXXXXXXXX+XXX:++======+X XX+.++X++XX+++X+XX++XXX++=++ XXXX+===--:--=++XXXXXXXXXXXXXX++++XX+X+++.=-------::::......\n",
|
||||
" \n \n \n \n \n \n \n :.... \n . . \n . ....- \n .:..:::::.. \n .:..+.::... . \n =::::.::... . +...::.. \n .. .:-==-:-:....::... .::. \n .:-::... .:-+=---::..:::......::.. ....+ . \n . . :..:-:::.... .:--=--::::::::-:.::+:-:.. :=-::. .. \n ....... ..::-:::.. .:::=--::::::.-:---=---:...:.:.:--:...- ..::..=. ..... \n . ..:::::::..... .. ...-:-::. ...-::-+=-=-::::::.:-+--=--::.....::-::... -..:------:.:.:... \n .. . ....:=::-::-:-...:..:..::..:--::. . .:::--+ +=----::.=.:-=:--::.+....+.-=::.=...:-=:==--+- ---::=... \n ............::::::--:-+-::=:::-:..=:...:--:::.... ..+---=++X+=-=-::..:.:==:=--:....::::==--::---=++.====--=---=:-:..: ... \n .....::::-::.:--::---====-------::.:::.---==--:.: ..-==--==-++====-::...::==----=::.:.::-==-::--=+=++++++======----: :. .... \n ....-:=::::: --:==----=+==++=+==+=- --=.=+=.==X+=--:: -===+==+++X+-+==-::--==.++=-:::. ..:-==++===+++XX+++X====+===----:::::.:...:.. \n ....::---------==:++==+++++++++X+=++XX+=======+XX++ ===+==-++==++X XXX++==++++++++=--:.:.::-==XX++.XXXXXXXX++=++:XX++==--::+: :::.... . \n .. .::---=========+X++XXX+XXXXXXX++:XXXX+=+=== =+X=X++X+-+.+++++XXXX++XXXX+++==++XX+==----- -=+XXXXXXXXXXXXX+++++X-XXX++=--------.:::......\n",
|
||||
" \n \n \n \n \n \n . \n . . \n :. .:.. \n .. .. ..::. \n . .....:: ..:. \n .-:...-.:.. . .. .. \n . .:-=-::... ...+. :+.. \n ... .:-+=-::::. ........:--.. ..-.. \n . .=-:.... .::==::::......-::::..-:. .::... \n .... ..::=. .:-==:=:.......::---::-. .:-::::. ........ \n ........ . ..=.:. .=::==--:+......:-+.--::. .+. ..::-:. .:-::::-:::.:. \n .....:-:.::-.. .. .. :. ..:.. .::--+=--:. .. ..:=::=::.......::-:.. ..:-----:::-=-:.:.:. \n ...... .::::::::.::--...... .....:-:.. .::--=+=.=::.... ..:=::.:-..:....::--+....:-=+=--==-==-::::::... \n ....-:.....::::::::::--- :::::.=....::--==-:.. . ..:=--+====--::.......-+-:.::+.. ...:=-:..::-.=======+-==----:::...: .. \n ......::...:-::--::--------==-=-:::---=++==++=-::-..:--:-=:==++===-:..::::==- ::.... .--=+.---=+X===+==+==+= ----::........ \n ...=:::-::::--=-==-=+==--====+++-:===-=-==.++X+=----=---======+XXXX+=-:-===++==--::. . .:--X+===.:XX+.+=+===+++====-::.::-:.+.... \n ...:::------+=-==+.++X+=++XXXXXX++=+X+===+=-==+XX++=+=++ =====+XXXXXX +===+==++X+==::....::-++X ++XXXXXXX+===-=++-+++=-=--=::::::=.-.... \n ...:-----=----==+X++XXXXXXXXXXX+XX+++++==+==-==+XX.+=++-=====+++XX:XXX-X+=+++=+XXX+=--======+++XXXXXXXXXX+===+++XX++++=+------::::=:.....\n"
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||
import ArrowRight from "@/components/app/(home)/sections/hero-input/_svg/ArrowRight";
|
||||
import Button from "@/components/shared/button/Button";
|
||||
|
||||
export default function HeroInputSubmitButton({
|
||||
dirty,
|
||||
buttonText = "Re-imagine Site",
|
||||
disabled = false,
|
||||
}: {
|
||||
dirty: boolean;
|
||||
buttonText?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
className={`hero-input-button !p-0 ${disabled ? 'bg-gray-400 hover:bg-gray-400 cursor-wait' : 'bg-heat-100 hover:bg-heat-200'}`}
|
||||
size="large"
|
||||
variant="primary"
|
||||
disabled={disabled}
|
||||
>
|
||||
<AnimatedWidth>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, x: -10, filter: "blur(2px)" }}
|
||||
initial={{ opacity: 0, x: 10, filter: "blur(2px)" }}
|
||||
key={dirty ? "dirty" : "clean"}
|
||||
>
|
||||
{dirty ? (
|
||||
<div className="py-8 w-126 text-center text-white">
|
||||
{buttonText}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-60 py-8 flex-center">
|
||||
<ArrowRight />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</AnimatedWidth>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import Globe from "./_svg/Globe";
|
||||
import HeroInputSubmitButton from "./Button/Button";
|
||||
import HeroInputTabsMobile from "./Tabs/Mobile/Mobile";
|
||||
import HeroInputTabs from "./Tabs/Tabs";
|
||||
import AsciiExplosion from "@/components/shared/effects/flame/ascii-explosion";
|
||||
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||
|
||||
export default function HeroInput() {
|
||||
const [tab, setTab] = useState<Endpoint>(Endpoint.Scrape);
|
||||
const [url, setUrl] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="max-w-552 mx-auto w-full z-[11] lg:z-[2] rounded-20 lg:-mt-76">
|
||||
<div
|
||||
className="overlay bg-accent-white"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 0px 44px 0px rgba(0, 0, 0, 0.02), 0px 88px 56px -20px rgba(0, 0, 0, 0.03), 0px 56px 56px -20px rgba(0, 0, 0, 0.02), 0px 32px 32px -20px rgba(0, 0, 0, 0.03), 0px 16px 24px -12px rgba(0, 0, 0, 0.03), 0px 0px 0px 1px rgba(0, 0, 0, 0.05), 0px 0px 0px 10px #F9F9F9",
|
||||
}}
|
||||
/>
|
||||
|
||||
<label className="p-16 flex gap-8 items-center w-full relative border-b border-black-alpha-5">
|
||||
<Globe />
|
||||
|
||||
<input
|
||||
className="w-full bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48"
|
||||
placeholder="https://example.com"
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
(
|
||||
document.querySelector(
|
||||
".hero-input-button",
|
||||
) as HTMLButtonElement
|
||||
)?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="p-10 flex justify-between items-center relative">
|
||||
<HeroInputTabs
|
||||
setTab={setTab}
|
||||
tab={tab}
|
||||
allowedModes={[
|
||||
Endpoint.Scrape,
|
||||
Endpoint.Search,
|
||||
Endpoint.Map,
|
||||
Endpoint.Crawl,
|
||||
]}
|
||||
/>
|
||||
|
||||
<HeroInputTabsMobile
|
||||
setTab={setTab}
|
||||
tab={tab}
|
||||
allowedModes={[
|
||||
Endpoint.Scrape,
|
||||
Endpoint.Search,
|
||||
Endpoint.Map,
|
||||
Endpoint.Crawl,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Link
|
||||
className="contents"
|
||||
href={`/playground?endpoint=${tab}&url=${url}&autorun=true`}
|
||||
>
|
||||
<HeroInputSubmitButton dirty={url.length > 0} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="h-248 top-84 cw-768 pointer-events-none absolute overflow-clip -z-10">
|
||||
<AsciiExplosion className="-top-200" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { tabs } from "@/components/app/(home)/sections/hero-input/Tabs/Tabs";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||
|
||||
export default function HeroInputTabsMobile(props: {
|
||||
setTab: (tab: Endpoint) => void;
|
||||
tab: Endpoint;
|
||||
allowedModes?: Endpoint[];
|
||||
}) {
|
||||
// Filter tabs based on allowedModes if provided
|
||||
const visibleTabs = props.allowedModes
|
||||
? tabs.filter((tab) => props.allowedModes!.includes(tab.value))
|
||||
: tabs;
|
||||
|
||||
const activeTab = visibleTabs.find((tab) => tab.value === props.tab)!;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth > 996) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (ref.current && e.composedPath().includes(ref.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="py-8 px-10 flex items-center rounded-10 inside-border before:border-black-alpha-4 relative lg:hidden gap-4"
|
||||
ref={ref}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<activeTab.icon size={24} alwaysHeat />
|
||||
<div className="px-6 text-label-medium">{activeTab.label}</div>
|
||||
<svg
|
||||
className={cn(
|
||||
"transition-all duration-200",
|
||||
isOpen ? "rotate-180 text-accent-black" : "text-black-alpha-48",
|
||||
)}
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.4001 10.2L12.0001 13.8L15.6001 10.2"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||
className="absolute z-[1001] top-[calc(100%-4px)] left-[calc(50%-(50vw-6px))] w-[calc(100vw-12px)]"
|
||||
exit={{ opacity: 0, filter: "blur(2px)" }}
|
||||
initial={{ opacity: 0, filter: "blur(2px)" }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: cubicBezier(0.25, 0.1, 0.25, 1.0),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto w-full p-4 max-w-366 rounded-16 bg-accent-white"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 32px 40px 6px rgba(0, 0, 0, 0.02), 0 12px 32px 0 rgba(0, 0, 0, 0.02), 0 24px 32px -8px rgba(0, 0, 0, 0.02), 0 8px 16px -2px rgba(0, 0, 0, 0.02), 0 0 0 1px rgba(0, 0, 0, 0.04)",
|
||||
}}
|
||||
>
|
||||
<div className="py-10 px-12 text-label-small text-black-alpha-48">
|
||||
Output
|
||||
</div>
|
||||
|
||||
<MenuItems
|
||||
setTab={props.setTab}
|
||||
tab={props.tab}
|
||||
visibleTabs={visibleTabs}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItems(props: {
|
||||
tab: Endpoint;
|
||||
setTab: (tab: Endpoint) => void;
|
||||
visibleTabs: typeof tabs;
|
||||
}) {
|
||||
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute top-0 opacity-0 left-0 bg-black-alpha-4 rounded-12 w-full pointer-events-none"
|
||||
ref={backgroundRef}
|
||||
/>
|
||||
|
||||
{props.visibleTabs.map((tab) => (
|
||||
<div
|
||||
className="text-label-small select-none cursor-pointer flex gap-12 py-12 px-16"
|
||||
key={tab.value}
|
||||
onClick={() => {
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
scaleX: [1, 0.99, 1],
|
||||
scaleY: [1, 0.96, 1],
|
||||
opacity: [1, 0.9, 1],
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.15,
|
||||
},
|
||||
);
|
||||
|
||||
props.setTab(tab.value);
|
||||
}}
|
||||
onMouseEnter={async (e) => {
|
||||
const child = e.currentTarget as HTMLElement;
|
||||
|
||||
if (backgroundRef.current?.getBoundingClientRect().height === 0) {
|
||||
backgroundRef.current!.style.height = child.offsetHeight + "px";
|
||||
}
|
||||
|
||||
if (getComputedStyle(backgroundRef.current!).opacity === "0") {
|
||||
await animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
y: child.offsetTop,
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.01,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
animate(backgroundRef.current!, { scale: 0.995 }).then(() =>
|
||||
animate(backgroundRef.current!, { scale: 1 }),
|
||||
);
|
||||
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
y: child.offsetTop,
|
||||
opacity: 1,
|
||||
height: child.offsetHeight + "px",
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.2,
|
||||
},
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.2,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="size-24 p-2">
|
||||
<tab.icon size={20} alwaysHeat />
|
||||
</div>
|
||||
<div className="px-6 text-label-medium">{tab.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { animate } from "motion";
|
||||
import { Fragment, useRef } from "react";
|
||||
|
||||
import EndpointsSearch from "@/components/app/(home)/sections/endpoints/EndpointsSearch/EndpointsSearch";
|
||||
import EndpointsCrawl from "@/components/app/(home)/sections/endpoints/EndpointsCrawl/EndpointsCrawl";
|
||||
import EndpointsMap from "@/components/app/(home)/sections/endpoints/EndpointsMap/EndpointsMap";
|
||||
import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
|
||||
import EndpointsExtract from "@/components/app/(home)/sections/endpoints/EndpointsExtract/EndpointsExtract";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import Tooltip from "@/components/ui/shadcn/tooltip";
|
||||
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||
|
||||
export const tabs = [
|
||||
{
|
||||
label: "Scrape",
|
||||
value: Endpoint.Scrape,
|
||||
action: "scraping",
|
||||
description:
|
||||
"Scrapes only the specified URL without crawling subpages. Outputs the content from the page.",
|
||||
icon: EndpointsScrape,
|
||||
},
|
||||
{
|
||||
label: "Search",
|
||||
value: Endpoint.Search,
|
||||
description: "Search the web and get full content from results",
|
||||
action: "searching",
|
||||
icon: EndpointsSearch,
|
||||
new: true,
|
||||
},
|
||||
{
|
||||
label: "Map",
|
||||
value: Endpoint.Map,
|
||||
action: "mapping",
|
||||
description: "Attempts to output all website's urls in a few seconds.",
|
||||
icon: EndpointsMap,
|
||||
},
|
||||
{
|
||||
label: "Crawl",
|
||||
value: Endpoint.Crawl,
|
||||
action: "crawling",
|
||||
description:
|
||||
"Crawls a URL and all its accessible subpages, outputting the content from each page.",
|
||||
icon: EndpointsCrawl,
|
||||
},
|
||||
{
|
||||
label: "Extract",
|
||||
value: Endpoint.Extract,
|
||||
action: "extracting",
|
||||
description:
|
||||
"Extract structured data from pages using LLMs. Provide URLs and a schema to get organized data.",
|
||||
icon: EndpointsExtract,
|
||||
new: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function HeroInputTabs(props: {
|
||||
setTab: (tab: Endpoint) => void;
|
||||
tab: Endpoint;
|
||||
disabled?: boolean;
|
||||
allowedModes?: Endpoint[];
|
||||
}) {
|
||||
// Filter tabs based on allowedModes if provided
|
||||
const visibleTabs = props.allowedModes
|
||||
? tabs.filter((tab) => props.allowedModes!.includes(tab.value))
|
||||
: tabs;
|
||||
|
||||
const activeIndex = visibleTabs.findIndex((tab) => tab.value === props.tab);
|
||||
|
||||
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-black-alpha-4 flex items-center rounded-10 p-2 relative lg-max:hidden"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 6px 12px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.75px 0.75px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.25px 0.25px 0px rgba(0, 0, 0, 0.04) inset",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute top-2 left-2 h-32 bg-accent-white rounded-8 w-89"
|
||||
ref={backgroundRef}
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 6px 12px -3px rgba(0, 0, 0, 0.04), 0px 3px 6px -1px rgba(0, 0, 0, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.04), 0px 0.5px 0.5px 0px rgba(0, 0, 0, 0.06)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{visibleTabs.map((tab, index) => (
|
||||
<Fragment key={tab.value}>
|
||||
{index > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"px-2 transition-all",
|
||||
!(index !== activeIndex && index !== activeIndex + 1) &&
|
||||
"opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="w-1 h-12 bg-black-alpha-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
"text-label-medium p-6 relative transition-all group flex items-center",
|
||||
tab.value === props.tab
|
||||
? "text-accent-black"
|
||||
: "text-black-alpha-56",
|
||||
!tab.new && "pr-4",
|
||||
)}
|
||||
key={tab.value}
|
||||
ref={(element) => {
|
||||
if (element && backgroundRef.current) {
|
||||
if (activeIndex === index) {
|
||||
animate(
|
||||
backgroundRef.current,
|
||||
{
|
||||
x: element.offsetLeft - 2,
|
||||
width: element.offsetWidth - 1,
|
||||
},
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
damping: 23,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
props.setTab(tab.value);
|
||||
|
||||
const t = e.target as HTMLElement;
|
||||
|
||||
const target =
|
||||
t instanceof HTMLButtonElement
|
||||
? t
|
||||
: (t.closest("button") as HTMLButtonElement);
|
||||
|
||||
if (backgroundRef.current) {
|
||||
animate(backgroundRef.current, { scale: 0.975 }).then(() =>
|
||||
animate(backgroundRef.current!, { scale: 1 }),
|
||||
);
|
||||
|
||||
animate(
|
||||
backgroundRef.current,
|
||||
{
|
||||
x: target.offsetLeft - 2,
|
||||
width: target.offsetWidth - 1,
|
||||
},
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 250,
|
||||
damping: 25,
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tab.icon && <tab.icon active={tab.value === props.tab} />}
|
||||
|
||||
<span className="px-6"> {tab.label}</span>
|
||||
|
||||
{tab.new && (
|
||||
<div
|
||||
className={cn(
|
||||
"py-2 px-6 rounded-4 text-[12px]/[16px] font-[450] transition-all",
|
||||
tab.value === props.tab
|
||||
? "bg-heat-12 text-heat-100"
|
||||
: "bg-black-alpha-4 text-black-alpha-56",
|
||||
)}
|
||||
>
|
||||
New
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tooltip delay={0.25} description={tab.description} offset={-8} />
|
||||
</button>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function Globe() {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 19.7083C16.2572 19.7083 19.7083 16.2572 19.7083 12C19.7083 7.74276 16.2572 4.29163 12 4.29163M12 19.7083C7.74276 19.7083 4.29163 16.2572 4.29163 12C4.29163 7.74276 7.74276 4.29163 12 4.29163M12 19.7083C10.044 19.7083 8.45829 16.2572 8.45829 12C8.45829 7.74276 10.044 4.29163 12 4.29163M12 19.7083C13.956 19.7083 15.5416 16.2572 15.5416 12C15.5416 7.74276 13.956 4.29163 12 4.29163M19.5 12H4.49996"
|
||||
stroke="#262626"
|
||||
strokeLinecap="square"
|
||||
strokeOpacity="0.32"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import CurvyRect, { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
|
||||
import HeroScrapingCodeLoading from "./Loading/Loading";
|
||||
import Code from "@/components/ui/code";
|
||||
|
||||
const URL = {
|
||||
value: "https://example.com",
|
||||
encrypted: "h=t*A:!/z!aap?A-cZz",
|
||||
};
|
||||
const MARKDOWN = {
|
||||
value: "# Getting Started...",
|
||||
encrypted: "# ?0z-ang S*a-Z-a0*9",
|
||||
};
|
||||
const TITLE = {
|
||||
value: "Guide",
|
||||
encrypted: "G!=*?",
|
||||
};
|
||||
const SCREENSHOT = {
|
||||
value: "https://example.com/hero",
|
||||
encrypted: "ht-=*:/?*Za!zl=-?a9?h0-!",
|
||||
};
|
||||
|
||||
export default function HeroScrapingCode({ step }: { step: number }) {
|
||||
const [url, setUrl] = useState(URL.encrypted);
|
||||
const [markdown, setMarkdown] = useState(MARKDOWN.encrypted);
|
||||
const [title, setTitle] = useState(TITLE.encrypted);
|
||||
const [screenshot, setScreenshot] = useState(SCREENSHOT.encrypted);
|
||||
|
||||
const reveal = useCallback((value: string, setter: (v: string) => void) => {
|
||||
let progress = 0;
|
||||
let increaseProgress = -10;
|
||||
|
||||
const animate = () => {
|
||||
increaseProgress = (increaseProgress + 1) % 5;
|
||||
|
||||
if (increaseProgress === 4) {
|
||||
progress += 0.2;
|
||||
}
|
||||
|
||||
if (progress > 1) {
|
||||
progress = 1;
|
||||
setter(encryptText(value, progress, { randomizeChance: 0.3 }));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setter(encryptText(value, progress, { randomizeChance: 0.3 }));
|
||||
|
||||
const interval = 70 + progress * 30;
|
||||
setTimeout(animate, interval);
|
||||
};
|
||||
|
||||
animate();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (step >= 0 && url === URL.encrypted) reveal(URL.value, setUrl);
|
||||
|
||||
if (step >= 3 && title === TITLE.encrypted) reveal(TITLE.value, setTitle);
|
||||
if (step >= 4 && markdown === MARKDOWN.encrypted)
|
||||
reveal(MARKDOWN.value, setMarkdown);
|
||||
|
||||
if (step >= 5 && screenshot === SCREENSHOT.encrypted)
|
||||
reveal(SCREENSHOT.value, setScreenshot);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (step < 0) {
|
||||
URL.encrypted = encryptText(URL.value, 0, { randomizeChance: 0.3 });
|
||||
setUrl(URL.encrypted);
|
||||
}
|
||||
|
||||
if (step < 3) {
|
||||
TITLE.encrypted = encryptText(TITLE.value, 0, { randomizeChance: 0.3 });
|
||||
setTitle(TITLE.encrypted);
|
||||
}
|
||||
|
||||
if (step < 4) {
|
||||
MARKDOWN.encrypted = encryptText(MARKDOWN.value, 0, {
|
||||
randomizeChance: 0.3,
|
||||
});
|
||||
setMarkdown(MARKDOWN.encrypted);
|
||||
}
|
||||
|
||||
if (step < 5) {
|
||||
SCREENSHOT.encrypted = encryptText(SCREENSHOT.value, 0, {
|
||||
randomizeChance: 0.3,
|
||||
});
|
||||
setScreenshot(SCREENSHOT.encrypted);
|
||||
}
|
||||
}, 70);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [step, reveal]);
|
||||
|
||||
return (
|
||||
<div className="h-280 lg:h-310 flex z-[1] w-full relative -top-1 bg-background-base">
|
||||
<Connector className="lg:hidden absolute -top-10 -left-[10.5px]" />
|
||||
<Connector className="lg:hidden absolute -top-10 -right-[10.5px]" />
|
||||
<div className="lg:hidden absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint" />
|
||||
|
||||
<Connector className="lg:hidden absolute -bottom-10 -left-[10.5px]" />
|
||||
<Connector className="lg:hidden absolute -bottom-10 -right-[10.5px]" />
|
||||
<div className="lg:hidden absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint" />
|
||||
|
||||
<div className="flex-1 lg-max:min-w-0 h-full relative lg:inside-border before:border-border-faint">
|
||||
<CurvyRect className="overlay" allSides />
|
||||
<CurvyRect
|
||||
className="size-32 absolute bottom-0 -left-31 lg-max:hidden"
|
||||
bottomRight
|
||||
/>
|
||||
|
||||
<div className="pl-15 border-b border-border-faint p-13 flex justify-between items-center">
|
||||
<div className="flex gap-10 items-center">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
className="w-12 h-12 rounded-full relative inside-border before:border-border-muted"
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-mono-x-small font-mono text-black-alpha-20">
|
||||
[ .JSON ]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-scroll hide-scrollbar lg:contents relative">
|
||||
<Code
|
||||
code={`[
|
||||
{
|
||||
"url": "${url}",
|
||||
"markdown": "${markdown}",
|
||||
"json": { "title": "${title}", "docs": "..." },
|
||||
"screenshot": "${screenshot}.png"
|
||||
}
|
||||
]`}
|
||||
language="json"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HeroScrapingCodeLoading finished={step >= 6} />
|
||||
</div>
|
||||
|
||||
<div className="w-28 lg-max:hidden -ml-1 relative">
|
||||
<div className="h-1 w-[calc(100%-1px)] top-0 left-0 absolute bg-border-faint" />
|
||||
<CurvyRect className="overlay" topLeft />
|
||||
</div>
|
||||
|
||||
<div className="h-53 lg-max:hidden -right-37 bottom-0 absolute w-65">
|
||||
<CurvyRect className="overlay" bottom topRight />
|
||||
<div className="overlay border-y border-border-faint" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||
import Spinner from "@/components/ui/spinner";
|
||||
|
||||
export default function HeroScrapingCodeLoading({
|
||||
finished,
|
||||
}: {
|
||||
finished: boolean;
|
||||
}) {
|
||||
const [scrapingText, setScrapingText] = useState("Scraping...");
|
||||
|
||||
useEffect(() => {
|
||||
if (finished) return;
|
||||
|
||||
let timeout = 0;
|
||||
let tick = 0;
|
||||
|
||||
const animate = () => {
|
||||
tick += 1;
|
||||
|
||||
if (tick % 3 !== 0) {
|
||||
setScrapingText(
|
||||
encryptText("Scraping", 0, {
|
||||
randomizeChance: 0.6 + Math.random() * 0.3,
|
||||
}) + "...",
|
||||
);
|
||||
} else {
|
||||
setScrapingText("Scraping...");
|
||||
}
|
||||
|
||||
const interval = 80;
|
||||
timeout = window.setTimeout(animate, interval);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
}, [finished]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 p-6 pr-0 rounded-full inside-border before:border-border-faint absolute right-20 bottom-20 text-mono-small font-mono text-accent-black">
|
||||
<Spinner finished={finished} />
|
||||
|
||||
<AnimatedWidth initial={{ width: "auto" }}>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="pr-12"
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
>
|
||||
{finished ? "Scrape Completed" : scrapingText}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</AnimatedWidth>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default function Check() {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M10 2.5C5.85786 2.5 2.5 5.85786 2.5 10C2.5 14.1421 5.85786 17.5 10 17.5C14.1421 17.5 17.5 14.1421 17.5 10C17.5 5.85786 14.1421 2.5 10 2.5ZM12.8305 8.59995C13.0928 8.27937 13.0455 7.80685 12.7249 7.54455C12.4043 7.28226 11.9318 7.32951 11.6695 7.65009L8.81932 11.1337L7.90533 10.2197C7.61244 9.9268 7.13756 9.9268 6.84467 10.2197C6.55178 10.5126 6.55178 10.9875 6.84467 11.2804L8.34467 12.7804C8.4945 12.9302 8.70073 13.0096 8.91236 12.9991C9.12399 12.9885 9.32129 12.8889 9.45547 12.725L12.8305 8.59995Z"
|
||||
fill="#FA5D19"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.hero-scraping-highlight::before {
|
||||
animation: hero-scraping-highlight-before 1s linear infinite;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@keyframes hero-scraping-highlight-before {
|
||||
0% {
|
||||
border-color: var(--border-loud);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 0.25;
|
||||
border-color: var(--heat-100);
|
||||
}
|
||||
|
||||
80% {
|
||||
border-color: var(--border-loud);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import CurvyRect from "@/components/shared/layout/curvy-rect";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import BrowserMobile from "./_svg/BrowserMobile";
|
||||
import BrowserTab from "./_svg/BrowserTab";
|
||||
import HeroScrapingCode from "./Code/Code";
|
||||
import HeroScrapingTag from "./Tag/Tag";
|
||||
|
||||
import "./HeroScraping.css";
|
||||
|
||||
export default function HeroScraping() {
|
||||
const [step, setStep] = useState(-1);
|
||||
|
||||
const navigationRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const h1Ref = useRef<HTMLDivElement>(null);
|
||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||
const ctaRef = useRef<HTMLDivElement>(null);
|
||||
const highlightRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wrapElement = async (
|
||||
element: HTMLElement,
|
||||
{ borderRadius }: { borderRadius?: number } = {},
|
||||
) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerBnds = containerRef.current.getBoundingClientRect();
|
||||
const elementBnds = element.getBoundingClientRect();
|
||||
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
try {
|
||||
if (highlightRef.current) {
|
||||
await animate(highlightRef.current, { opacity: 0 }, { duration: 0.3 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error animating highlight:", error);
|
||||
}
|
||||
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
Object.assign(highlightRef.current.style, {
|
||||
left: elementBnds.left - containerBnds.left - 4 + "px",
|
||||
top: elementBnds.top - containerBnds.top - 4 + "px",
|
||||
width: elementBnds.width + 8 + "px",
|
||||
height: elementBnds.height + 8 + "px",
|
||||
borderRadius: borderRadius ? `${borderRadius}px` : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
await animate(
|
||||
highlightRef.current,
|
||||
{ opacity: [1, 0.5, 0.3, 0.8, 0.6, 0.9, 0.7, 1] },
|
||||
{ duration: 0.4 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error animating highlight:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
setStep(0);
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
await animate(highlightRef.current, {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
await sleep(700);
|
||||
|
||||
setTimeout(() => setStep(1), 300);
|
||||
if (navigationRef.current) {
|
||||
await wrapElement(navigationRef.current);
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(2), 300);
|
||||
if (buttonRef.current) {
|
||||
await wrapElement(buttonRef.current);
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(3), 300);
|
||||
if (h1Ref.current) {
|
||||
await wrapElement(h1Ref.current, { borderRadius: 12 });
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(4), 300);
|
||||
if (descriptionRef.current) {
|
||||
await wrapElement(descriptionRef.current, { borderRadius: 8 });
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(5), 300);
|
||||
if (ctaRef.current) {
|
||||
await wrapElement(ctaRef.current, { borderRadius: 24 });
|
||||
}
|
||||
|
||||
await sleep(1500);
|
||||
setTimeout(() => setStep(6), 300);
|
||||
|
||||
if (highlightRef.current) {
|
||||
await animate(highlightRef.current, { opacity: 0 }, { duration: 0.3 });
|
||||
}
|
||||
};
|
||||
|
||||
let started = false;
|
||||
|
||||
const onScroll = () => {
|
||||
if (started) return;
|
||||
|
||||
if (window.scrollY > 100) {
|
||||
started = true;
|
||||
start();
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (started) return;
|
||||
|
||||
started = true;
|
||||
start();
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
}, 2000);
|
||||
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pt-56 lg:pt-25 lg:px-25 container -mt-36 relative"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="h-53 absolute top-[calc(100%-1px)] w-full left-0">
|
||||
<div className="h-1 bg-border-faint bottom-0 left-0 w-full absolute" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="left-61 top-89 rounded-[16px] size-32 absolute hero-scraping-highlight inside-border before:border-border-loud opacity-0 scale-[0.9]"
|
||||
ref={highlightRef}
|
||||
/>
|
||||
|
||||
<div className="overlay lg-max:hidden">
|
||||
<div className="h-1 absolute bottom-0 w-full left-0 bg-border-faint" />
|
||||
<CurvyRect className="overlay" bottom />
|
||||
</div>
|
||||
|
||||
<div className="lg:h-370 rounded-t-16 lg-max:pt-70 relative">
|
||||
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b from-black/7 to-transparent" />
|
||||
|
||||
<div className="top-17 left-17 flex gap-8 items-center absolute lg-max:hidden">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
className="w-10 h-10 rounded-full relative inside-border before:border-border-muted"
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-42 lg:px-6">
|
||||
<BrowserMobile className="absolute top-0 cw-316 lg:hidden" />
|
||||
|
||||
<BrowserTab className="absolute top-[7.5px] left-70 lg-max:hidden bg-background-base z-[1]" />
|
||||
<div className="absolute size-18 top-17 left-89 lg-max:hidden inside-border before:border-border-muted z-[2] rounded-full" />
|
||||
|
||||
<div className="rounded-t-16 relative lg:h-330 lg:p-6">
|
||||
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b from-black/7 to-transparent" />
|
||||
|
||||
<div className="lg:h-322 rounded-t-10 relative">
|
||||
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b z-[2] from-black/7 to-transparent" />
|
||||
|
||||
<div className="px-28 lg-max:hidden py-20 flex justify-between items-center relative border-b border-border-faint">
|
||||
<div className="flex gap-8 items-center relative">
|
||||
<div className="size-24 rounded-full relative inside-border before:border-border-muted" />
|
||||
<div className="w-64 h-12 rounded-full relative inside-border before:border-border-muted" />
|
||||
|
||||
{step >= 0 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 0}
|
||||
className="absolute left-[calc(100%+24px)] top-0"
|
||||
initial={{ x: -12, opacity: 0 }}
|
||||
label="Logo"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute top-24 center-x flex gap-8"
|
||||
ref={navigationRef}
|
||||
>
|
||||
{step >= 1 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 1}
|
||||
className="absolute right-[calc(100%+20px)] -top-4"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="Navigation"
|
||||
/>
|
||||
)}
|
||||
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
className="w-64 h-16 rounded-full relative inside-border before:border-border-muted"
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-72 h-24 rounded-full relative inside-border before:border-border-muted"
|
||||
ref={buttonRef}
|
||||
>
|
||||
{step >= 2 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 2}
|
||||
className="absolute right-[calc(100%+20px)] top-0"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="Button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:grid grid-cols-2">
|
||||
<div className="pt-40 pl-151 flex gap-16 relative lg-max:hidden">
|
||||
<CurvyRect
|
||||
className="size-32 -top-1 -right-1 absolute"
|
||||
topRight
|
||||
/>
|
||||
|
||||
<div className="h-53 lg-max:hidden -left-37 bottom-1 absolute w-65">
|
||||
<CurvyRect className="overlay" left />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="flex gap-16 mb-16 flex-wrap w-300 relative"
|
||||
ref={h1Ref}
|
||||
>
|
||||
{step >= 3 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 3}
|
||||
className="absolute right-[calc(100%+16px)] top-0"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="H1 Title"
|
||||
/>
|
||||
)}
|
||||
<div className="w-144 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||
<div className="w-82 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||
<div className="w-100 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||
<div className="w-180 h-32 rounded-8 relative inside-border before:border-border-muted" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex gap-6 mb-32 flex-wrap w-300 relative"
|
||||
ref={descriptionRef}
|
||||
>
|
||||
{step >= 4 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 4}
|
||||
className="absolute top-0 right-[calc(100%+16px)]"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="Description"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="w-131 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||
<div className="w-72 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||
<div className="w-34 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||
<div className="w-56 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||
<div className="w-116 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||
<div className="w-116 h-10 rounded-full relative inside-border before:border-border-muted" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-64 h-24 rounded-full relative inside-border before:border-border-muted"
|
||||
ref={ctaRef}
|
||||
>
|
||||
{step >= 5 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 5}
|
||||
className="absolute top-0 right-[calc(100%+16px)]"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="CTA Button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroScrapingCode step={step} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ComponentProps, useEffect, useState } from "react";
|
||||
|
||||
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export default function HeroScrapingTag({
|
||||
active,
|
||||
label,
|
||||
...attrs
|
||||
}: ComponentProps<typeof motion.div> & { active?: boolean; label: string }) {
|
||||
const [value, setValue] = useState(
|
||||
encryptText(label, 0, { randomizeChance: 0 }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let progress = 0;
|
||||
let increaseProgress = -10;
|
||||
|
||||
const animate = () => {
|
||||
increaseProgress = (increaseProgress + 1) % 5;
|
||||
|
||||
if (increaseProgress === 4) {
|
||||
progress += 0.2;
|
||||
}
|
||||
|
||||
if (progress > 1) {
|
||||
progress = 1;
|
||||
setValue(encryptText(label, progress, { randomizeChance: 0 }));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(encryptText(label, progress, { randomizeChance: 0 }));
|
||||
|
||||
const interval = 40 + progress * 20;
|
||||
setTimeout(animate, interval);
|
||||
};
|
||||
|
||||
animate();
|
||||
}, [label]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
{...attrs}
|
||||
animate={{
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: "blur(0px)",
|
||||
}}
|
||||
className={cn(
|
||||
"py-4 h-max font-mono w-max px-6 text-mono-x-small rounded-6 transition-colors",
|
||||
active
|
||||
? "bg-heat-12 text-heat-100"
|
||||
: "bg-black-alpha-4 text-black-alpha-56",
|
||||
attrs.className,
|
||||
)}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 18,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
export default function BrowserMobile(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="112"
|
||||
viewBox="0 0 316 112"
|
||||
width="316"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_2254_6088)">
|
||||
<rect
|
||||
height="370"
|
||||
rx="15.5"
|
||||
stroke="url(#paint0_linear_2254_6088)"
|
||||
strokeOpacity="0.07"
|
||||
width="315"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
/>
|
||||
<mask fill="white" id="path-2-inside-1_2254_6088">
|
||||
<path d="M240 32C240 37.5228 244.477 42 250 42H294C302.837 42 310 49.1634 310 58V361C310 366.523 305.523 371 300 371H16C10.4772 371 6 366.523 6 361V58C6 49.1634 13.1634 42 22 42H70C75.5228 42 80 37.5228 80 32V18C80 12.4772 84.4772 8 90 8H230C235.523 8 240 12.4772 240 18V32Z" />
|
||||
</mask>
|
||||
<path
|
||||
d="M310 58L311 58L310 58ZM22 42L22 41L22 42ZM250 42V43H294V42V41H250V42ZM294 42V43C302.284 43 309 49.7157 309 58L310 58L311 58C311 48.6112 303.389 41 294 41V42ZM310 58H309V361H310H311V58H310ZM300 371V370H16V371V372H300V371ZM6 361H7V58H6H5V361H6ZM6 58H7C7 49.7157 13.7157 43 22 43L22 42L22 41C12.6112 41 5 48.6112 5 58H6ZM22 42V43H70V42V41H22V42ZM80 32H81V18H80H79V32H80ZM90 8V9H230V8V7H90V8ZM240 18H239V32H240H241V18H240ZM230 8V9C234.971 9 239 13.0294 239 18H240H241C241 11.9249 236.075 7 230 7V8ZM70 42V43C76.0751 43 81 38.0751 81 32H80H79C79 36.9706 74.9706 41 70 41V42ZM16 371V370C11.0294 370 7 365.971 7 361H6H5C5 367.075 9.92487 372 16 372V371ZM80 18H81C81 13.0294 85.0294 9 90 9V8V7C83.9249 7 79 11.9249 79 18H80ZM310 361H309C309 365.971 304.971 370 300 370V371V372C306.075 372 311 367.075 311 361H310ZM250 42V41C245.029 41 241 36.9706 241 32H240H239C239 38.0751 243.925 43 250 43V42Z"
|
||||
fill="url(#paint1_linear_2254_6088)"
|
||||
fillOpacity="0.07"
|
||||
mask="url(#path-2-inside-1_2254_6088)"
|
||||
/>
|
||||
<rect
|
||||
height="310"
|
||||
rx="9.5"
|
||||
stroke="url(#paint2_linear_2254_6088)"
|
||||
strokeOpacity="0.07"
|
||||
width="291"
|
||||
x="12.5"
|
||||
y="48.5"
|
||||
/>
|
||||
<rect
|
||||
height="9"
|
||||
rx="4.5"
|
||||
stroke="#E8E8E8"
|
||||
width="9"
|
||||
x="17.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<rect
|
||||
height="9"
|
||||
rx="4.5"
|
||||
stroke="#E8E8E8"
|
||||
width="9"
|
||||
x="35.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<rect
|
||||
height="9"
|
||||
rx="4.5"
|
||||
stroke="#E8E8E8"
|
||||
width="9"
|
||||
x="53.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<rect
|
||||
height="17"
|
||||
rx="8.5"
|
||||
stroke="#E8E8E8"
|
||||
width="17"
|
||||
x="89.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<mask fill="white" id="path-10-inside-2_2254_6088">
|
||||
<path d="M12 48H304V112H12V48Z" />
|
||||
</mask>
|
||||
<path
|
||||
d="M304 112V111H12V112V113H304V112Z"
|
||||
fill="#EDEDED"
|
||||
mask="url(#path-10-inside-2_2254_6088)"
|
||||
/>
|
||||
<rect
|
||||
height="23"
|
||||
rx="11.5"
|
||||
stroke="#E8E8E8"
|
||||
width="71"
|
||||
x="212.5"
|
||||
y="68.5"
|
||||
/>
|
||||
<circle cx="44" cy="80" r="11.5" stroke="#E8E8E8" />
|
||||
<rect
|
||||
height="11"
|
||||
rx="5.5"
|
||||
stroke="#E8E8E8"
|
||||
width="63"
|
||||
x="64.5"
|
||||
y="74.5"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="paint0_linear_2254_6088"
|
||||
x1="158"
|
||||
x2="158"
|
||||
y1="0"
|
||||
y2="371"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="paint1_linear_2254_6088"
|
||||
x1="529.5"
|
||||
x2="529.5"
|
||||
y1="8"
|
||||
y2="324"
|
||||
>
|
||||
<stop offset="0.4" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="paint2_linear_2254_6088"
|
||||
x1="158"
|
||||
x2="158"
|
||||
y1="48"
|
||||
y2="359"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2254_6088">
|
||||
<rect fill="white" height="112" width="316" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
export default function BrowserTab(attrs: HTMLAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="36"
|
||||
viewBox="0 0 226 36"
|
||||
width="226"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...attrs}
|
||||
>
|
||||
<path
|
||||
d="M0 35C5.52285 35 10 30.5228 10 25V11C10 5.47715 14.4772 1 20 1H206C211.523 1 216 5.47715 216 11V25C216 30.5228 220.477 35 226 35"
|
||||
stroke="#E8E8E8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import CurvyRect from "@/components/shared/layout/curvy-rect";
|
||||
|
||||
import CenterStar from "./_svg/CenterStar";
|
||||
|
||||
export default function HomeHeroBackground() {
|
||||
return (
|
||||
<div className="overlay contain-layout pointer-events-none lg-max:hidden">
|
||||
<div className="top-100 h-[calc(100%-99px)] border-border-faint border-y w-full left-0 absolute" />
|
||||
|
||||
<div className="cw-[1314px] z-[105] absolute top-0 border-x border-border-faint h-full">
|
||||
<div className="text-mono-x-small font-mono text-black-alpha-12 select-none">
|
||||
<div className="absolute top-111 -left-1 w-102 text-center">
|
||||
{" "}
|
||||
[ 200 OK ]{" "}
|
||||
</div>
|
||||
<div className="absolute bottom-10 -left-1 w-102 text-center">
|
||||
{" "}
|
||||
[ .JSON ]{" "}
|
||||
</div>
|
||||
|
||||
<div className="absolute top-111 -right-1 w-102 text-center">
|
||||
{" "}
|
||||
[ SCRAPE ]{" "}
|
||||
</div>
|
||||
<div className="absolute bottom-10 -right-1 w-102 text-center">
|
||||
{" "}
|
||||
[ .MD ]{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="top-302 h-1 left-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-403 h-1 left-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-504 h-1 left-100 bg-border-faint w-203 absolute" />
|
||||
|
||||
<div className="top-302 h-1 right-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-403 h-1 right-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-504 h-1 right-100 bg-border-faint w-203 absolute" />
|
||||
|
||||
{Array.from({ length: 2 }, (_, i) => (
|
||||
<Fragment key={i}>
|
||||
<CurvyRect
|
||||
bottomLeft={i === 1}
|
||||
bottomRight={i === 0}
|
||||
className="w-101 h-[calc(100%-99px)] top-100 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -101 }}
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="w-102 h-203 top-100 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||
allSides
|
||||
/>
|
||||
<CurvyRect
|
||||
className="size-102 top-302 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||
allSides
|
||||
/>
|
||||
<CurvyRect
|
||||
className="w-102 h-203 top-403 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||
allSides
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cw-[910px] absolute top-100 border-x border-border-faint h-[calc(100%-99px)]" />
|
||||
<div className="cw-[708px] absolute top-100 border-x border-border-faint h-[calc(100%-99px)]">
|
||||
<CenterStar className="absolute top-77 -right-24 z-[1]" />
|
||||
<CenterStar className="absolute top-77 -left-24 z-[1]" />
|
||||
</div>
|
||||
|
||||
<CurvyRect
|
||||
className="cw-[708px] absolute top-100 h-[calc(100%-99px)]"
|
||||
bottom
|
||||
/>
|
||||
|
||||
<div className="cw-[506px] absolute top-100 border-x border-border-faint h-102" />
|
||||
<div className="cw-[304px] absolute top-100 border-x border-border-faint h-102" />
|
||||
<div className="cw-[102px] absolute top-100 border-x border-border-faint h-102" />
|
||||
|
||||
<div className="top-201 h-1 bg-border-faint cw-[1112px] absolute" />
|
||||
|
||||
<div className="cw-[1112px] absolute top-0 h-full">
|
||||
<CurvyRect className="w-full absolute top-full h-100 left-0" top />
|
||||
<CurvyRect
|
||||
className="w-100 absolute top-full h-100 -left-99"
|
||||
topRight
|
||||
/>
|
||||
<CurvyRect
|
||||
className="w-100 absolute top-full h-100 -right-99"
|
||||
topLeft
|
||||
/>
|
||||
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Fragment key={i}>
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-0"
|
||||
style={{
|
||||
top: 100 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-0"
|
||||
style={{
|
||||
top: 100 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-101 top-100"
|
||||
bottomLeft
|
||||
top
|
||||
/>
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-101 top-201"
|
||||
bottom
|
||||
topLeft
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-101 top-100"
|
||||
bottomRight
|
||||
top
|
||||
/>
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-101 top-201"
|
||||
bottom
|
||||
topRight
|
||||
/>
|
||||
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
<Fragment key={i}>
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-101"
|
||||
style={{
|
||||
top: 302 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-101"
|
||||
style={{
|
||||
top: 302 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute top-100 left-202"
|
||||
bottomRight
|
||||
top
|
||||
/>
|
||||
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<CurvyRect
|
||||
className="size-102 absolute top-100"
|
||||
key={i}
|
||||
style={{ left: 303 + i * 101 }}
|
||||
allSides
|
||||
/>
|
||||
))}
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute top-100 right-202"
|
||||
bottomLeft
|
||||
top
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import {
|
||||
useHeaderContext,
|
||||
useHeaderHeight,
|
||||
} from "@/components/shared/header/HeaderContext";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export const BackgroundOuterPiece = () => {
|
||||
const [noRender, setNoRender] = useState(false);
|
||||
const { dropdownContent } = useHeaderContext();
|
||||
const { headerHeight } = useHeaderHeight();
|
||||
|
||||
useEffect(() => {
|
||||
const heroContent = document.getElementById("hero-content");
|
||||
if (!heroContent) {
|
||||
// If hero-content doesn't exist, don't render the background piece
|
||||
setNoRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const heroContentHeight = heroContent.clientHeight;
|
||||
|
||||
const onScroll = () => {
|
||||
setNoRender(window.scrollY > heroContentHeight - 120);
|
||||
};
|
||||
|
||||
onScroll();
|
||||
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"cw-[1335px] transition-all z-[105] absolute top-0 flex justify-between h-[calc(100%+21px)] duration-[200ms] pointer-events-none",
|
||||
{ "opacity-0": noRender || dropdownContent || !headerHeight },
|
||||
)}
|
||||
style={{
|
||||
paddingTop: headerHeight - 10,
|
||||
}}
|
||||
>
|
||||
<div className="h-[3000px] w-[calc(100%-21px)] left-[10.5px] absolute bottom-21 border-x border-border-faint" />
|
||||
|
||||
<Connector className="sticky" style={{ top: headerHeight - 10 }} />
|
||||
<Connector className="sticky" style={{ top: headerHeight - 10 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
export default function CenterStar({
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="47"
|
||||
viewBox="0 0 47 47"
|
||||
width="47"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M24 18C24 21.3137 26.6863 24 30 24H34V25H30C26.6863 25 24 27.6863 24 31V35H23V31C23 27.6863 20.3137 25 17 25H13V24H17C20.3137 24 23 21.3137 23 18V14H24V18Z"
|
||||
fill="var(--heat-100)"
|
||||
fillOpacity="1"
|
||||
/>
|
||||
<circle cx="23.5" cy="23.5" r="23" stroke="#EDEDED" strokeOpacity="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HomeHeroBadge() {
|
||||
return (
|
||||
<Link
|
||||
className="p-4 rounded-full flex w-max mx-auto mb-12 lg:mb-16 items-center relative inside-border before:border-border-faint group"
|
||||
href="#"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="px-8 text-label-x-small">Website Builder</div>
|
||||
|
||||
<div className="p-1">
|
||||
<div className="size-18 bg-accent-black flex-center rounded-full group-hover:bg-heat-100 transition-all group-hover:w-30">
|
||||
<svg
|
||||
fill="none"
|
||||
height="8"
|
||||
viewBox="0 0 10 8"
|
||||
width="10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="transition-all -translate-x-2 group-hover:translate-x-0"
|
||||
d="M6 1L9 4L6 7"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
|
||||
<path
|
||||
className="transition-all -translate-x-3 group-hover:translate-x-0 scale-x-[0] group-hover:scale-x-[1] origin-right"
|
||||
d="M1 4L9 4"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
|
||||
|
||||
import HomeHeroBackground from "./Background/Background";
|
||||
import { BackgroundOuterPiece } from "./Background/BackgroundOuterPiece";
|
||||
import HomeHeroBadge from "./Badge/Badge";
|
||||
import HomeHeroPixi from "./Pixi/Pixi";
|
||||
import HomeHeroTitle from "./Title/Title";
|
||||
import HeroInput from "../hero-input/HeroInput";
|
||||
import HeroScraping from "../hero-scraping/HeroScraping";
|
||||
|
||||
export default function HomeHero() {
|
||||
return (
|
||||
<section className="overflow-x-clip" id="home-hero">
|
||||
<div
|
||||
className="pt-28 lg:pt-254 lg:-mt-100 pb-115 relative"
|
||||
id="hero-content"
|
||||
>
|
||||
<HomeHeroPixi />
|
||||
<HeroFlame />
|
||||
|
||||
<BackgroundOuterPiece />
|
||||
|
||||
<HomeHeroBackground />
|
||||
|
||||
<div className="relative container px-16">
|
||||
<HomeHeroBadge />
|
||||
<HomeHeroTitle />
|
||||
|
||||
<p className="text-center text-body-large">
|
||||
Power your AI apps with clean data crawled
|
||||
<br className="lg-max:hidden" />
|
||||
from any website.
|
||||
<Link
|
||||
className="bg-black-alpha-4 hover:bg-black-alpha-6 lg:ml-4 rounded-6 px-8 lg:px-6 text-label-large lg-max:py-2 h-30 lg:h-24 block lg-max:mt-8 lg-max:mx-auto lg-max:w-max lg:inline-block gap-4 transition-all"
|
||||
href="https://github.com/firecrawl/firecrawl"
|
||||
target="_blank"
|
||||
>
|
||||
It's also open source.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container lg:contents !p-16 relative -mt-90">
|
||||
<div className="absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
||||
<div className="absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
||||
|
||||
<Connector className="-top-10 -left-[10.5px] lg:hidden" />
|
||||
<Connector className="-top-10 -right-[10.5px] lg:hidden" />
|
||||
<Connector className="-bottom-10 -left-[10.5px] lg:hidden" />
|
||||
<Connector className="-bottom-10 -right-[10.5px] lg:hidden" />
|
||||
|
||||
<HeroInput />
|
||||
</div>
|
||||
|
||||
<HeroScraping />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, lazy, useState, useEffect } from "react";
|
||||
|
||||
const Pixi = lazy(() => import("@/components/shared/pixi/Pixi"));
|
||||
import features from "./tickers/features";
|
||||
|
||||
function PixiContent() {
|
||||
return (
|
||||
<Pixi
|
||||
canvasAttrs={{
|
||||
className: "cw-[1314px] h-506 absolute top-100 lg-max:hidden",
|
||||
}}
|
||||
fps={Infinity}
|
||||
initOptions={{ backgroundAlpha: 0 }}
|
||||
smartStop={false}
|
||||
tickers={[features]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomeHeroPixi() {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleError = (e: ErrorEvent) => {
|
||||
if (e.message.includes('pixi') || e.message.includes('ChunkLoadError')) {
|
||||
setHasError(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleError);
|
||||
return () => window.removeEventListener('error', handleError);
|
||||
}, []);
|
||||
|
||||
if (hasError) {
|
||||
// Return empty div as fallback if Pixi fails to load
|
||||
return <div className="cw-[1314px] h-506 absolute top-100 lg-max:hidden" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div className="cw-[1314px] h-506 absolute top-100 lg-max:hidden" />}>
|
||||
<PixiContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import PixiAssetManager from "@/components/shared/pixi/PixiAssetManager";
|
||||
import { RenderTexture, Sprite, Text } from "pixi.js";
|
||||
|
||||
// Add more contrast to the ASCII_CHARS and ensure 'X' is used
|
||||
// const ASCII_CHARS = [' ', '.', ':', '-', '=', '+', 'X'];
|
||||
const ASCII_CHARS = ' .":,-_^=+';
|
||||
|
||||
function getAsciiChar(luminance: number) {
|
||||
if (luminance < 50) return " ";
|
||||
|
||||
const norm = Math.max(0, Math.min(1, (luminance - 16) / (250 - 16)));
|
||||
const skewed = Math.pow(norm, 1.5);
|
||||
|
||||
const minIdx = 1;
|
||||
const maxIdx = ASCII_CHARS.length - 1;
|
||||
const idx = minIdx + Math.floor(skewed * (maxIdx - minIdx + 1));
|
||||
const safeIdx = Math.max(minIdx, Math.min(maxIdx, idx));
|
||||
|
||||
return ASCII_CHARS[safeIdx];
|
||||
}
|
||||
|
||||
// Sprinkle logic is now a no-op, as getAsciiChar handles the randomness
|
||||
function sprinkleAscii(line: string) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const tickAscii: Ticker = async ({ app, canvas }) => {
|
||||
const textures = await Promise.all(
|
||||
Array.from({ length: 150 }, async (_, i) => {
|
||||
const texture = await PixiAssetManager.load(
|
||||
`/Arşiv/FAQ Demo/FAQ_${i.toString().padStart(5, "0")}.png`,
|
||||
);
|
||||
|
||||
return texture!;
|
||||
}),
|
||||
);
|
||||
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
|
||||
const sprites = textures.map((texture) => new Sprite(texture));
|
||||
|
||||
sprites.forEach((sprite) => {
|
||||
sprite.width = width;
|
||||
sprite.height = height;
|
||||
sprite.x = 0;
|
||||
sprite.y = 0;
|
||||
|
||||
app.stage.addChild(sprite);
|
||||
sprite.alpha = 0;
|
||||
});
|
||||
|
||||
// Render the texture to a renderTexture to extract pixels
|
||||
const renderTexture = RenderTexture.create({ width, height });
|
||||
|
||||
let ascii = "";
|
||||
|
||||
const asciiText = new Text({
|
||||
text: ascii,
|
||||
style: {
|
||||
fontFamily: "monospace",
|
||||
fontSize: 8,
|
||||
fill: 0x000,
|
||||
align: "left",
|
||||
lineHeight: 8,
|
||||
whiteSpace: "pre",
|
||||
},
|
||||
});
|
||||
asciiText.alpha = 0.2;
|
||||
asciiText.x = 0;
|
||||
asciiText.y = 0;
|
||||
|
||||
const variants: string[] = [];
|
||||
|
||||
const render = (index: number) => {
|
||||
ascii = "";
|
||||
const sprite = sprites[index];
|
||||
|
||||
sprites.forEach((sprite) => {
|
||||
sprite.alpha = 0;
|
||||
});
|
||||
|
||||
sprite.alpha = 1;
|
||||
app.renderer.render({ container: sprite, target: renderTexture });
|
||||
sprite.alpha = 0;
|
||||
|
||||
const pixels = app.renderer.extract.pixels(renderTexture).pixels;
|
||||
|
||||
const charWidth = 4.81640625;
|
||||
|
||||
for (let y = 0; y < height; y += 8) {
|
||||
let line = "";
|
||||
|
||||
for (let x = 0; x < width; x += charWidth) {
|
||||
let totalLum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let dy = 0; dy < 8; dy++) {
|
||||
for (let dx = 0; dx < 4; dx++) {
|
||||
const px = Math.floor(x + dx);
|
||||
const py = Math.floor(y + dy);
|
||||
if (px >= width || py >= height) continue;
|
||||
const idx = (py * width + px) * 4;
|
||||
const r = pixels[idx];
|
||||
const g = pixels[idx + 1];
|
||||
const b = pixels[idx + 2];
|
||||
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
totalLum += lum;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
const avgLum = count ? totalLum / count : 0;
|
||||
line += getAsciiChar(avgLum);
|
||||
}
|
||||
ascii += sprinkleAscii(line) + "\n";
|
||||
}
|
||||
|
||||
variants[index] = ascii;
|
||||
|
||||
asciiText.text = ascii;
|
||||
};
|
||||
|
||||
app.stage.addChild(asciiText);
|
||||
|
||||
for (let i = 0; i < sprites.length; i++) {
|
||||
render(i);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
//@ts-expect-error - safeAdd method exists on extended ticker
|
||||
app.ticker.safeAdd(() => {
|
||||
i++;
|
||||
if (i >= sprites.length) i = 0;
|
||||
|
||||
render(i);
|
||||
});
|
||||
};
|
||||
|
||||
export default tickAscii;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
|
||||
import AnimatedRect from "./components/AnimatedRect";
|
||||
import BlinkingContainer from "./components/BlinkingContainer";
|
||||
import crawl from "./crawl";
|
||||
import mapping from "./mapping";
|
||||
import scrape from "./scrape";
|
||||
import search from "./search";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export const CELL_SIZE = 80;
|
||||
|
||||
export const MAIN_COLOR = 0xe6e6e6;
|
||||
|
||||
const animations = [scrape, mapping, search, crawl];
|
||||
|
||||
let lastActive = -1;
|
||||
|
||||
export default function cell(props: Props) {
|
||||
const blinkingContainer = BlinkingContainer({
|
||||
x: props.x + 10,
|
||||
y: props.y + 10,
|
||||
app: props.app,
|
||||
});
|
||||
|
||||
const anchorGraphic = AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 4,
|
||||
height: 4,
|
||||
radius: 10,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
|
||||
blinkingContainer.container.addChild(anchorGraphic.graphic);
|
||||
|
||||
props.app.stage.addChild(blinkingContainer.container);
|
||||
|
||||
let running = false;
|
||||
|
||||
return {
|
||||
trigger: async () => {
|
||||
if (running) return;
|
||||
|
||||
running = true;
|
||||
|
||||
lastActive = (lastActive + 1) % animations.length;
|
||||
|
||||
const fn = animations[lastActive];
|
||||
|
||||
await fn({
|
||||
...props,
|
||||
blinkingContainer,
|
||||
anchorGraphic,
|
||||
});
|
||||
|
||||
running = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import AnimatedRect from "./components/AnimatedRect";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export default function cellReveal(props: Props) {
|
||||
const graphic = AnimatedRect({
|
||||
app: props.app,
|
||||
x: props.x + 0.5,
|
||||
y: props.y + 0.5,
|
||||
width: 101,
|
||||
height: 101,
|
||||
radius: 0,
|
||||
alpha: 0,
|
||||
color: 0x000,
|
||||
centering: false,
|
||||
});
|
||||
|
||||
props.app.stage.addChild(graphic.graphic);
|
||||
|
||||
return {
|
||||
trigger: async () => {
|
||||
let cycleCount = 0;
|
||||
|
||||
const cycle = async () => {
|
||||
await graphic.animate(
|
||||
{
|
||||
alpha: Math.random() * 0.04,
|
||||
},
|
||||
{
|
||||
ease: "linear",
|
||||
duration: 0.03,
|
||||
},
|
||||
);
|
||||
|
||||
if (cycleCount < 5) {
|
||||
cycleCount += 1;
|
||||
cycle();
|
||||
} else {
|
||||
await graphic.animate({ alpha: 0 });
|
||||
graphic.graphic.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
cycle();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
import { AnimationOptions, cubicBezier } from "motion";
|
||||
import { Application, Container, Graphics, Sprite } from "pixi.js";
|
||||
|
||||
import { isDestroyed } from "@/components/shared/pixi/utils";
|
||||
|
||||
type Props = {
|
||||
app: Application;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
radius: number;
|
||||
color: number;
|
||||
scale?: number;
|
||||
rotation?: number;
|
||||
type?: "rect" | "arc" | "container" | Sprite;
|
||||
animationConfig?: AnimationOptions;
|
||||
alpha?: number;
|
||||
|
||||
centering?: boolean;
|
||||
};
|
||||
|
||||
export type IAnimatedRect = ReturnType<typeof AnimatedRect>;
|
||||
|
||||
export default function AnimatedRect(props: Props) {
|
||||
const graphic = (() => {
|
||||
if (props.type === "container") return new Container();
|
||||
if (props.type instanceof Sprite) return props.type;
|
||||
|
||||
return new Graphics();
|
||||
})();
|
||||
|
||||
props.alpha ??= 1;
|
||||
props.scale ??= 1;
|
||||
props.centering ??= true;
|
||||
props.rotation ??= 0;
|
||||
|
||||
const p = {
|
||||
...props,
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (isDestroyed(props.app) || graphic.destroyed) return;
|
||||
|
||||
graphic.scale.set(p.scale!);
|
||||
graphic.alpha = p.alpha!;
|
||||
graphic.rotation = p.rotation!;
|
||||
|
||||
if (!(graphic instanceof Graphics)) {
|
||||
if (graphic instanceof Sprite) {
|
||||
graphic.x = p.x;
|
||||
graphic.y = p.y;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const g = graphic as Graphics;
|
||||
|
||||
g.clear();
|
||||
|
||||
if (p.type !== "arc") {
|
||||
g.roundRect(
|
||||
p.centering ? p.x - p.width / 2 : p.x,
|
||||
p.centering ? p.y - p.height / 2 : p.y,
|
||||
p.width,
|
||||
p.height,
|
||||
p.radius,
|
||||
);
|
||||
} else {
|
||||
g.arc(p.x, p.y, p.width / 2, 0, Math.PI * 2);
|
||||
}
|
||||
|
||||
g.fill({ color: p.color });
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
p.animationConfig ??= {
|
||||
duration: 0.4,
|
||||
ease: cubicBezier(0.83, 0, 0.17, 1),
|
||||
};
|
||||
|
||||
return {
|
||||
defaultProps: props,
|
||||
currentProps: p,
|
||||
graphic,
|
||||
setStyle: (style: Partial<Props>) => {
|
||||
Object.assign(p, style);
|
||||
|
||||
render();
|
||||
},
|
||||
render,
|
||||
animate: (renderProps: Partial<Props>, settings?: AnimationOptions) =>
|
||||
(props.app as any).animate(p, renderProps, {
|
||||
...p.animationConfig,
|
||||
...settings,
|
||||
onUpdate: render,
|
||||
}),
|
||||
reset: () => (props.app as any).animate(p, props, { onUpdate: render }),
|
||||
};
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
|
||||
import { Application, Graphics } from "pixi.js";
|
||||
|
||||
import { CELL_SIZE } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
|
||||
|
||||
import AnimatedRect from "./AnimatedRect";
|
||||
|
||||
export type IBlinkingContainer = ReturnType<typeof BlinkingContainer>;
|
||||
|
||||
export default function BlinkingContainer({
|
||||
x,
|
||||
y,
|
||||
app,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
app: Application;
|
||||
}) {
|
||||
const animatedRect = AnimatedRect({
|
||||
app,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: CELL_SIZE,
|
||||
height: CELL_SIZE,
|
||||
radius: 0,
|
||||
color: 0xededed,
|
||||
type: "container",
|
||||
});
|
||||
|
||||
animatedRect.graphic.pivot.set(CELL_SIZE / 2, CELL_SIZE / 2);
|
||||
|
||||
animatedRect.graphic.x = x + CELL_SIZE / 2;
|
||||
animatedRect.graphic.y = y + CELL_SIZE / 2;
|
||||
|
||||
animatedRect.graphic.addChild(
|
||||
new Graphics()
|
||||
.rect(0, 0, CELL_SIZE, CELL_SIZE)
|
||||
.fill({ color: "#EDEDED", alpha: 0 }),
|
||||
);
|
||||
|
||||
const blinkLayer = new Graphics()
|
||||
.rect(0, 0, CELL_SIZE, CELL_SIZE)
|
||||
.fill({ color: "#F9F9F9" });
|
||||
|
||||
blinkLayer.zIndex = 1;
|
||||
blinkLayer.alpha = 0;
|
||||
|
||||
animatedRect.graphic.addChild(blinkLayer);
|
||||
|
||||
return {
|
||||
container: animatedRect.graphic,
|
||||
animate: animatedRect.animate,
|
||||
reset: animatedRect.reset,
|
||||
shrink: async () => {
|
||||
await animatedRect.animate({ scale: 0.92 });
|
||||
|
||||
animatedRect.animate({ scale: 1 });
|
||||
},
|
||||
blink: ({ delay = 0 }: { delay?: number } = {}) => {
|
||||
(app as any)
|
||||
.animate(0, 0.32, {
|
||||
repeatType: "reverse",
|
||||
repeat: 2,
|
||||
delay,
|
||||
duration: 0.065,
|
||||
ease: "linear",
|
||||
onUpdate: (value: any) => {
|
||||
blinkLayer.alpha = value as number;
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
(app as any).animate(0.32, 0, {
|
||||
duration: 0.065,
|
||||
ease: "linear",
|
||||
onUpdate: (value: any) => {
|
||||
blinkLayer.alpha = value as number;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MAIN_COLOR } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
|
||||
|
||||
import AnimatedRect from "./AnimatedRect";
|
||||
|
||||
export default function Dot(
|
||||
props: Pick<
|
||||
Parameters<typeof AnimatedRect>[0],
|
||||
"x" | "y" | "app" | "animationConfig"
|
||||
>,
|
||||
) {
|
||||
return AnimatedRect({
|
||||
...props,
|
||||
width: 2,
|
||||
height: 2,
|
||||
radius: 10,
|
||||
color: MAIN_COLOR,
|
||||
type: "arc",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { animate } from "motion";
|
||||
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function crawl(props: Props) {
|
||||
const rects = Array.from({ length: 6 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 8,
|
||||
height: 8,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 16 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
/* Step 1: Reveal the main square, reveal the corner dots */
|
||||
await Promise.all(
|
||||
[
|
||||
dots[0].animate({ x: 30, y: 30 }, { delay: 0.2 }),
|
||||
dots[1].animate({ x: CELL_SIZE - 30, y: 30 }, { delay: 0.2 }),
|
||||
dots[2].animate({ x: 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||
dots[3].animate({ x: CELL_SIZE - 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||
|
||||
props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
props.blinkingContainer.blink({ delay: 0.3 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
let spriteOverlay: IAnimatedRect | null = null;
|
||||
|
||||
// Use fallback rectangle instead of trying to load missing image
|
||||
spriteOverlay = AnimatedRect({
|
||||
x: 13,
|
||||
y: 39,
|
||||
color: MAIN_COLOR,
|
||||
width: 54,
|
||||
height: 34,
|
||||
app: props.app,
|
||||
radius: 4,
|
||||
centering: false,
|
||||
});
|
||||
|
||||
spriteOverlay.graphic.zIndex = -1;
|
||||
props.blinkingContainer.container.addChild(spriteOverlay.graphic);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
spriteOverlay?.animate({ height: 23, y: 50 }),
|
||||
|
||||
rects[0].animate({ width: 16, height: 16, y: 34 }),
|
||||
rects.slice(1, 4).map((rect) => rect.animate({ x: 24, y: 50 })),
|
||||
rects.slice(4, 8).map((rect) => rect.animate({ x: 56, y: 50 })),
|
||||
|
||||
dots[0].animate({ x: 28, y: 22 }),
|
||||
dots[1].animate({ x: 52, y: 22 }),
|
||||
dots[2].animate({ x: 16, y: 58 }),
|
||||
dots[3].animate({ x: 64, y: 58 }),
|
||||
|
||||
dots[4].animate({ x: 16, y: 42 }),
|
||||
dots[5].animate({ x: 64, y: 42 }),
|
||||
dots[6].animate({ x: 32, y: 58 }),
|
||||
dots[7].animate({ x: 48, y: 58 }),
|
||||
|
||||
dots.slice(8, 12).map((dot) => dot.animate({ x: 24, y: 50 })),
|
||||
dots.slice(12, 16).map((dot) => dot.animate({ x: 56, y: 50 })),
|
||||
].flat().filter(Boolean),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
props.blinkingContainer.blink({ delay: 0.3 });
|
||||
await props.blinkingContainer.shrink();
|
||||
try {
|
||||
await Promise.all(
|
||||
[
|
||||
spriteOverlay?.animate({ height: 8, y: 58 }),
|
||||
|
||||
rects[0].animate({ y: 28 }),
|
||||
[1, 4].map((i) => rects[i].animate({ y: 44 })),
|
||||
[2, 3].map((i) => rects[i].animate({ x: 12, y: 56 })),
|
||||
[5, 6].map((i) => rects[i].animate({ x: 68, y: 56 })),
|
||||
|
||||
dots[0].animate({ y: 16 }),
|
||||
dots[1].animate({ y: 16 }),
|
||||
|
||||
dots[2].animate({ x: 4, y: 64 }),
|
||||
dots[3].animate({ x: 76, y: 64 }),
|
||||
|
||||
dots[4].animate({ x: 4, y: 48 }),
|
||||
dots[5].animate({ x: 76, y: 48 }),
|
||||
dots[6].animate({ x: 20, y: 64 }),
|
||||
dots[7].animate({ x: 60, y: 64 }),
|
||||
|
||||
dots[8].animate({ x: 16, y: 36 }),
|
||||
dots[12].animate({ x: 64, y: 36 }),
|
||||
|
||||
dots[9].animate({ x: 32, y: 52 }),
|
||||
dots[13].animate({ x: 48, y: 52 }),
|
||||
|
||||
[10, 11].map((i) => dots[i].animate({ x: 12, y: 56 })),
|
||||
[14, 15].map((i) => dots[i].animate({ x: 68, y: 56 })),
|
||||
].flat().filter(Boolean),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
props.blinkingContainer.blink({ delay: 0.3 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
spriteOverlay.animate({ height: 0, y: 66 }),
|
||||
|
||||
rects[0].animate({ y: 20 }),
|
||||
[1, 4].map((i) => rects[i].animate({ y: 36 })),
|
||||
[2, 5].map((i) => rects[i].animate({ y: 48 })),
|
||||
[3, 6].map((i) => rects[i].animate({ y: 60, x: i === 3 ? 24 : 56 })),
|
||||
|
||||
[0, 1, 4, 5, 8, 9, 12, 13].map((i) =>
|
||||
dots[i].animate({ y: dots[i].currentProps.y - 8 }),
|
||||
),
|
||||
|
||||
dots[2].animate({ x: 4, y: 56 }),
|
||||
dots[3].animate({ x: 76, y: 56 }),
|
||||
|
||||
dots[6].animate({ x: 32, y: 68 }),
|
||||
dots[7].animate({ x: 48, y: 68 }),
|
||||
|
||||
dots[10].animate({ x: 32, y: 52 }),
|
||||
dots[11].animate({ x: 16, y: 68 }),
|
||||
dots[14].animate({ x: 48, y: 52 }),
|
||||
dots[15].animate({ x: 64, y: 68 }),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
dots.map((dot) =>
|
||||
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
spriteOverlay.graphic.destroy();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import setTimeoutOnVisible from "@/utils/set-timeout-on-visible";
|
||||
|
||||
import cell from "./cell";
|
||||
import cellReveal from "./cellReveal";
|
||||
|
||||
const CELL_GRID = [
|
||||
"-ooooooooooo-",
|
||||
"-oo-------oo-",
|
||||
"ooo-------ooo",
|
||||
"-oo-------oo-",
|
||||
"-oo-------oo-",
|
||||
];
|
||||
|
||||
const REVEAL_ANIMATION_GRID = [
|
||||
[
|
||||
"---ooooooo---",
|
||||
"--o-------o--",
|
||||
"--o-------o--",
|
||||
"--o-------o--",
|
||||
"--o-------o--",
|
||||
],
|
||||
[
|
||||
"--o-------o--",
|
||||
"-o---------o-",
|
||||
"-o---------o-",
|
||||
"-o---------o-",
|
||||
"-o---------o-",
|
||||
],
|
||||
[
|
||||
"-o---------o-",
|
||||
"-------------",
|
||||
"o-----------o",
|
||||
"-------------",
|
||||
"-------------",
|
||||
],
|
||||
[
|
||||
"-------------",
|
||||
"-------------",
|
||||
"o-----------o",
|
||||
"-------------",
|
||||
"-------------",
|
||||
],
|
||||
];
|
||||
|
||||
const features: Ticker = (params) => {
|
||||
const cells: ReturnType<typeof cell>[] = [];
|
||||
const cellReveals: {
|
||||
cell: ReturnType<typeof cellReveal>;
|
||||
row: number;
|
||||
column: number;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < CELL_GRID.length; i++) {
|
||||
const row = CELL_GRID[i];
|
||||
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
if (row[j] === "o") {
|
||||
cells.push(
|
||||
cell({
|
||||
...params,
|
||||
x: j * 101,
|
||||
y: i * 101,
|
||||
}),
|
||||
);
|
||||
|
||||
cellReveals.push({
|
||||
cell: cellReveal({
|
||||
...params,
|
||||
x: j * 101,
|
||||
y: i * 101,
|
||||
}),
|
||||
row: i,
|
||||
column: j,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cycle = () =>
|
||||
setTimeoutOnVisible({
|
||||
element: params.canvas,
|
||||
callback: () => {
|
||||
const cell = cells[Math.floor(Math.random() * cells.length)];
|
||||
|
||||
if (cell) {
|
||||
cell.trigger().then(() => cycle());
|
||||
}
|
||||
},
|
||||
timeout: 3000 * Math.random(),
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
cycle();
|
||||
}
|
||||
|
||||
let revealIndex = -1;
|
||||
|
||||
const revealCycle = () => {
|
||||
revealIndex += 1;
|
||||
|
||||
for (let i = 0; i < REVEAL_ANIMATION_GRID[revealIndex].length; i++) {
|
||||
const row = REVEAL_ANIMATION_GRID[revealIndex][i];
|
||||
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
if (row[j] === "o") {
|
||||
cellReveals
|
||||
.find((cell) => cell.row === i && cell.column === j)
|
||||
?.cell.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (revealIndex < REVEAL_ANIMATION_GRID.length - 1) {
|
||||
setTimeout(() => {
|
||||
revealCycle();
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
revealCycle();
|
||||
};
|
||||
|
||||
export default features;
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function mapping(props: Props) {
|
||||
const rects = Array.from({ length: 8 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 20 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
});
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
dots.slice(0, 16).map((dot, index) => {
|
||||
const x = 13 + (index % 4) * 18;
|
||||
const y = 13 + Math.floor(index / 4) * 18;
|
||||
|
||||
return dot.animate({ x, y });
|
||||
}),
|
||||
|
||||
rects[0].animate({ width: 10, height: 10 }),
|
||||
|
||||
rects.map((rect, index) => {
|
||||
const x = 22 + (index % 3) * 18;
|
||||
const y = 22 + Math.floor(index / 3) * 18;
|
||||
|
||||
return rect.animate({ x, y });
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(300);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
const baseDotPositions = [
|
||||
[13, 13],
|
||||
[31, 31],
|
||||
[49, 31],
|
||||
[13, 31],
|
||||
[49, 49],
|
||||
[67, 49],
|
||||
[13, 49],
|
||||
[31, 67],
|
||||
[67, 67],
|
||||
];
|
||||
|
||||
const dotPositions: string[] = [];
|
||||
|
||||
for (const [x, y] of baseDotPositions) {
|
||||
const positions = [
|
||||
{ x: x - 9, y: y - 9 },
|
||||
{ x: x + 9, y: y - 9 },
|
||||
{ x: x - 9, y: y + 9 },
|
||||
{ x: x + 9, y: y + 9 },
|
||||
];
|
||||
|
||||
for (const position of positions) {
|
||||
if (!dotPositions.includes(`${position.x},${position.y}`)) {
|
||||
dotPositions.push(`${position.x},${position.y}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects[0].animate({ x: 13, y: 13 }),
|
||||
rects[1].animate({ x: 31, y: 31 }),
|
||||
rects[2].animate({ x: 49, y: 31 }),
|
||||
rects[3].animate({ x: 13, y: 31 }),
|
||||
|
||||
rects[4].animate({ x: 49, y: 49 }),
|
||||
rects[5].animate({ x: 67, y: 49 }),
|
||||
|
||||
rects[6].animate({ x: 13, y: 49 }),
|
||||
rects[7].animate({ x: 31, y: 67 }),
|
||||
rects[8].animate({ x: 67, y: 67 }),
|
||||
|
||||
dots.map((dot, index) => {
|
||||
const position = dotPositions[index].split(",").map(Number);
|
||||
|
||||
return dot.animate({ x: position[0], y: position[1] });
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const lines = Array.from({ length: 8 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
centering: false,
|
||||
animationConfig: {
|
||||
duration: 0.25,
|
||||
ease: "linear",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
lines.forEach((graphic) =>
|
||||
props.blinkingContainer.container.addChild(graphic.graphic),
|
||||
);
|
||||
|
||||
(async () => {
|
||||
lines[0].setStyle({ width: 1, height: 0, y: 18, x: 12.5 });
|
||||
await lines[0].animate({ height: 9 });
|
||||
|
||||
lines[1].setStyle({ width: 0, height: 1, y: 30.5, x: 18 });
|
||||
await lines[1].animate({ width: 9 });
|
||||
lines[2].setStyle({ width: 0, height: 1, y: 30.5, x: 36 });
|
||||
await lines[2].animate({ width: 9 });
|
||||
|
||||
lines[3].setStyle({ width: 1, height: 3, y: 36, x: 48.5 });
|
||||
await lines[3].animate({ height: 9 });
|
||||
lines[4].setStyle({ width: 0, height: 1, y: 48.5, x: 54 });
|
||||
await lines[4].animate({ width: 9 });
|
||||
})();
|
||||
|
||||
lines[5].setStyle({ width: 0, height: 1, y: 66.5, x: 62 });
|
||||
await lines[5].animate({ width: 28, x: 62 - 28 }, { duration: 0.4 });
|
||||
lines[6].setStyle({ width: 0, height: 1, y: 66.5, x: 26 });
|
||||
await lines[6].animate({ width: 13.5, x: 26 - 13.5 });
|
||||
lines[7].setStyle({ width: 1, height: 0, y: 66.5, x: 12.5 });
|
||||
await lines[7].animate({ height: 14.5, y: 66.5 - 13.5 });
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
lines.map((line) => line.animate({ alpha: 0 })),
|
||||
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
|
||||
dots.map((dot) =>
|
||||
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
lines.forEach((line) => line.graphic.destroy());
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function scrape(props: Props) {
|
||||
const rects = Array.from({ length: 15 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 25 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
[0, 12, 13, 14].map((index) =>
|
||||
dots[index].animate({ x: 30, y: 30 }, { delay: 0.2 }),
|
||||
),
|
||||
[1, 15, 16, 17].map((index) =>
|
||||
dots[index].animate({ x: CELL_SIZE - 30, y: 30 }, { delay: 0.2 }),
|
||||
),
|
||||
[2, 18, 19, 20].map((index) =>
|
||||
dots[index].animate({ x: 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||
),
|
||||
[3, 21, 22, 23].map((index) =>
|
||||
dots[index].animate(
|
||||
{ x: CELL_SIZE - 30, y: CELL_SIZE - 30 },
|
||||
{ delay: 0.2 },
|
||||
),
|
||||
),
|
||||
|
||||
props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
[0, 12, 13, 14].map((index) => dots[index].animate({ x: 22, y: 22 })),
|
||||
[1, 15, 16, 17].map((index) =>
|
||||
dots[index].animate({ x: CELL_SIZE - 22, y: 22 }),
|
||||
),
|
||||
[2, 18, 19, 20].map((index) =>
|
||||
dots[index].animate({ x: 22, y: CELL_SIZE - 22 }),
|
||||
),
|
||||
[3, 21, 22, 23].map((index) =>
|
||||
dots[index].animate({ x: CELL_SIZE - 22, y: CELL_SIZE - 22 }),
|
||||
),
|
||||
|
||||
dots[4].animate({ x: 40, y: 22 }),
|
||||
dots[5].animate({ x: 22, y: 40 }),
|
||||
dots[6].animate({ x: CELL_SIZE - 22, y: 40 }),
|
||||
dots[7].animate({ x: 40, y: 58 }),
|
||||
|
||||
dots[8].animate({ x: 40, y: 22 }),
|
||||
dots[9].animate({ x: 22, y: 40 }),
|
||||
dots[10].animate({ x: CELL_SIZE - 22, y: 40 }),
|
||||
dots[11].animate({ x: 40, y: 58 }),
|
||||
|
||||
rects[0].animate({ width: 10, height: 10 }),
|
||||
rects.slice(0, 4).map((rect) => rect.animate({ x: 31, y: 31 })),
|
||||
rects
|
||||
.slice(4, 8)
|
||||
.map((rect) => rect.animate({ x: CELL_SIZE - 31, y: 31 })),
|
||||
rects
|
||||
.slice(8, 12)
|
||||
.map((rect) => rect.animate({ x: 31, y: CELL_SIZE - 31 })),
|
||||
rects
|
||||
.slice(12, 16)
|
||||
.map((rect) => rect.animate({ x: CELL_SIZE - 31, y: CELL_SIZE - 31 })),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
dots[0].animate({ x: 4, y: 4 }),
|
||||
dots[1].animate({ x: CELL_SIZE - 4, y: 4 }),
|
||||
dots[2].animate({ x: 4, y: CELL_SIZE - 4 }),
|
||||
dots[3].animate({ x: CELL_SIZE - 4, y: CELL_SIZE - 4 }),
|
||||
dots[4].animate({ x: 40, y: 4 }),
|
||||
dots[5].animate({ x: 4, y: 40 }),
|
||||
dots[6].animate({ x: 76, y: 40 }),
|
||||
dots[7].animate({ x: 40, y: 76 }),
|
||||
|
||||
dots[13].animate({ x: 22, y: 4 }),
|
||||
dots[14].animate({ x: 4, y: 22 }),
|
||||
dots[16].animate({ x: 58, y: 4 }),
|
||||
dots[17].animate({ x: 76, y: 22 }),
|
||||
dots[19].animate({ x: 4, y: 58 }),
|
||||
dots[20].animate({ x: 22, y: 76 }),
|
||||
dots[22].animate({ x: 58, y: 76 }),
|
||||
dots[23].animate({ x: 76, y: 58 }),
|
||||
|
||||
rects.map((rect, index) => {
|
||||
const quadrant = Math.floor(index / 4);
|
||||
const position = index % 4;
|
||||
|
||||
const col = (position % 2 === 0 ? 1 : 2) + (quadrant % 2 === 0 ? 0 : 2);
|
||||
const row = Math.floor(position / 2) + (quadrant < 2 ? 1 : 3);
|
||||
|
||||
return rect.animate({
|
||||
x: 13 + (col - 1) * 18,
|
||||
y: 13 + (row - 1) * 18,
|
||||
});
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
Promise.all(
|
||||
dots.map((dot) =>
|
||||
dot.animate({ alpha: 0 }, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
);
|
||||
|
||||
await sleep(100);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.2 });
|
||||
|
||||
const newWidths: number[] = [];
|
||||
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
newWidths.push(20 + Math.random() * 28);
|
||||
} else {
|
||||
const remainingSpace = 62 - newWidths[i - 1];
|
||||
newWidths.push(10 + Math.random() * remainingSpace);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
rects.map((rect, index) => {
|
||||
const y = 8 + Math.floor(index / 2) * 6 + Math.floor(index / 4) * 8;
|
||||
|
||||
return rect.animate(
|
||||
{
|
||||
y,
|
||||
x:
|
||||
(index % 2 === 0 ? 8 : newWidths[index - 1] + 10) +
|
||||
newWidths[index] / 2,
|
||||
height: 4,
|
||||
width: newWidths[index],
|
||||
},
|
||||
{
|
||||
delay: Math.random() * 0.1,
|
||||
},
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function search(props: Props) {
|
||||
const rects = Array.from({ length: 8 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 16 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
});
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
dots.map((dot, index) => {
|
||||
const x = 13 + (index % 4) * 18;
|
||||
const y = 13 + Math.floor(index / 4) * 18;
|
||||
|
||||
return dot.animate({ x, y });
|
||||
}),
|
||||
|
||||
rects[0].animate({ width: 10, height: 10 }),
|
||||
|
||||
rects.map((rect, index) => {
|
||||
const x = 22 + (index % 3) * 18;
|
||||
const y = 22 + Math.floor(index / 3) * 18;
|
||||
|
||||
return rect.animate({ x, y });
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(300);
|
||||
|
||||
Promise.all(
|
||||
[
|
||||
rects.map((rect) => rect.animate({ alpha: 0.68 })),
|
||||
dots.map((dot) => dot.animate({ alpha: 0.68 })),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
props.blinkingContainer.blink();
|
||||
await sleep(400);
|
||||
|
||||
for await (const rect of rects) {
|
||||
// Get the surrounding dots of this rect
|
||||
const rectX = rect.currentProps.x;
|
||||
const rectY = rect.currentProps.y;
|
||||
const surroundingDots = dots.filter((dot) => {
|
||||
const dx = Math.abs(dot.currentProps.x - rectX);
|
||||
const dy = Math.abs(dot.currentProps.y - rectY);
|
||||
|
||||
// Consider "surrounding" as adjacent horizontally, vertically, or diagonally (distance 18)
|
||||
return (
|
||||
(dx === 0 && dy === 9) ||
|
||||
(dx === 9 && dy === 0) ||
|
||||
(dx === 9 && dy === 9)
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
surroundingDots.map((dot) =>
|
||||
dot.animate({ alpha: 1 }, { duration: 0.75 }),
|
||||
),
|
||||
rect.animate({ alpha: 1, width: 14, height: 14 }, { duration: 0.75 }),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rect.animate({ alpha: 0.68, width: 10, height: 10 }, { duration: 0.75 });
|
||||
Promise.all(
|
||||
surroundingDots.map((dot) =>
|
||||
dot.animate({ alpha: 0.68 }, { duration: 0.75 }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
|
||||
dots.map((dot) =>
|
||||
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
// import dynamic from "next/dynamic";
|
||||
// import { useRef, useEffect, forwardRef } from "react";
|
||||
|
||||
// const originalText =
|
||||
// "";
|
||||
|
||||
type Options = {
|
||||
randomizeChance?: number;
|
||||
reversed?: boolean;
|
||||
};
|
||||
|
||||
export const encryptText = (
|
||||
text: string,
|
||||
progress: number,
|
||||
_options?: Options,
|
||||
) => {
|
||||
const options = {
|
||||
randomizeChance: 0.7,
|
||||
..._options,
|
||||
};
|
||||
|
||||
const encryptionChars = "a-zA-Z0-9*=?!";
|
||||
const skipTags = ["<br class='lg-max:hidden'>", "<span>", "</span>"];
|
||||
|
||||
// Calculate how many characters should be encrypted
|
||||
const totalChars = text.length;
|
||||
const encryptedCount = Math.floor(totalChars * (1 - progress));
|
||||
|
||||
let result = "";
|
||||
let charIndex = 1;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
|
||||
// Check if we're at the start of a tag to skip
|
||||
let shouldSkip = false;
|
||||
|
||||
for (const tag of skipTags) {
|
||||
if (text.substring(i, i + tag.length) === tag) {
|
||||
result += tag;
|
||||
i += tag.length - 1; // -1 because loop will increment
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSkip) continue;
|
||||
|
||||
// Skip spaces - keep them as is
|
||||
if (char === " ") {
|
||||
result += char;
|
||||
charIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this character should be encrypted
|
||||
if (
|
||||
options.reversed
|
||||
? charIndex < encryptedCount
|
||||
: text.length - charIndex < encryptedCount
|
||||
) {
|
||||
// 40% chance to show original character, 60% chance to encrypt
|
||||
if (Math.random() < options.randomizeChance) {
|
||||
result += char;
|
||||
} else {
|
||||
// Use random character from encryption set
|
||||
const randomIndex = Math.floor(Math.random() * encryptionChars.length);
|
||||
result += encryptionChars[randomIndex];
|
||||
}
|
||||
} else {
|
||||
// Keep original character
|
||||
result += char;
|
||||
}
|
||||
|
||||
charIndex++;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// const Wrapper = forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >((props, ref) => {
|
||||
// return (
|
||||
// <div className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||
// <div {...props} className="hidden lg:contents" ref={ref} />
|
||||
// <div
|
||||
// className="lg:hidden contents"
|
||||
// dangerouslySetInnerHTML={{ __html: originalText }}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
// Wrapper.displayName = "Wrapper";
|
||||
|
||||
// export default dynamic(() => Promise.resolve(HomeHeroTitle), {
|
||||
// ssr: false,
|
||||
// loading: () => (
|
||||
// <Wrapper
|
||||
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||
// />
|
||||
// ),
|
||||
// });
|
||||
|
||||
// function HomeHeroTitle() {
|
||||
// const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (window.innerWidth < 996) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let progress = 0;
|
||||
// let increaseProgress = -10;
|
||||
|
||||
// const animate = () => {
|
||||
// increaseProgress = (increaseProgress + 1) % 5;
|
||||
|
||||
// if (increaseProgress === 4) {
|
||||
// progress += 0.3;
|
||||
// }
|
||||
|
||||
// if (progress > 1) {
|
||||
// progress = 1;
|
||||
// textRef.current!.innerHTML = encryptText(originalText, progress);
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
// textRef.current!.innerHTML = encryptText(originalText, progress);
|
||||
|
||||
// const interval = 50 + progress * 20;
|
||||
// setTimeout(animate, interval);
|
||||
// };
|
||||
|
||||
// animate();
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <Wrapper
|
||||
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||
// ref={textRef}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// import dynamic from "next/dynamic";
|
||||
// import { useRef, useEffect, forwardRef } from "react";
|
||||
|
||||
// const originalText =
|
||||
// "Turn websites into <br class='lg-max:hidden'><span>LLM-ready</span> data";
|
||||
|
||||
// type Options = {
|
||||
// randomizeChance?: number;
|
||||
// reversed?: boolean;
|
||||
// };
|
||||
|
||||
// export const encryptText = (
|
||||
// text: string,
|
||||
// progress: number,
|
||||
// _options?: Options,
|
||||
// ) => {
|
||||
// const options = {
|
||||
// randomizeChance: 0.7,
|
||||
// ..._options,
|
||||
// };
|
||||
|
||||
// const encryptionChars = "a-zA-Z0-9*=?!";
|
||||
// const skipTags = ["<br class='lg-max:hidden'>", "<span>", "</span>"];
|
||||
|
||||
// // Calculate how many characters should be encrypted
|
||||
// const totalChars = text.length;
|
||||
// const encryptedCount = Math.floor(totalChars * (1 - progress));
|
||||
|
||||
// let result = "";
|
||||
// let charIndex = 1;
|
||||
|
||||
// for (let i = 0; i < text.length; i++) {
|
||||
// const char = text[i];
|
||||
|
||||
// // Check if we're at the start of a tag to skip
|
||||
// let shouldSkip = false;
|
||||
|
||||
// for (const tag of skipTags) {
|
||||
// if (text.substring(i, i + tag.length) === tag) {
|
||||
// result += tag;
|
||||
// i += tag.length - 1; // -1 because loop will increment
|
||||
// shouldSkip = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (shouldSkip) continue;
|
||||
|
||||
// // Skip spaces - keep them as is
|
||||
// if (char === " ") {
|
||||
// result += char;
|
||||
// charIndex++;
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // If this character should be encrypted
|
||||
// if (
|
||||
// options.reversed
|
||||
// ? charIndex < encryptedCount
|
||||
// : text.length - charIndex < encryptedCount
|
||||
// ) {
|
||||
// // 40% chance to show original character, 60% chance to encrypt
|
||||
// if (Math.random() < options.randomizeChance) {
|
||||
// result += char;
|
||||
// } else {
|
||||
// // Use random character from encryption set
|
||||
// const randomIndex = Math.floor(Math.random() * encryptionChars.length);
|
||||
// result += encryptionChars[randomIndex];
|
||||
// }
|
||||
// } else {
|
||||
// // Keep original character
|
||||
// result += char;
|
||||
// }
|
||||
|
||||
// charIndex++;
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// const Wrapper = forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >((props, ref) => {
|
||||
// return (
|
||||
// <div className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||
// <div {...props} className="hidden lg:contents" ref={ref} />
|
||||
// <div
|
||||
// className="lg:hidden contents"
|
||||
// dangerouslySetInnerHTML={{ __html: originalText }}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
// Wrapper.displayName = "Wrapper";
|
||||
|
||||
// export default dynamic(() => Promise.resolve(HomeHeroTitle), {
|
||||
// ssr: false,
|
||||
// loading: () => (
|
||||
// <Wrapper
|
||||
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||
// />
|
||||
// ),
|
||||
// });
|
||||
|
||||
export default function HomeHeroTitle() {
|
||||
return (
|
||||
<h1 className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||
Open Lovable <span>v3</span>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Home Page Components Rules
|
||||
|
||||
When working with home/landing page components in components-new/app/(home):
|
||||
|
||||
## Structure
|
||||
```
|
||||
home/
|
||||
├── sections/ # Major page sections
|
||||
│ ├── hero/ # Hero section with flames
|
||||
│ ├── features/ # Feature showcase
|
||||
│ ├── testimonials/# Customer testimonials
|
||||
│ ├── pricing/ # Pricing cards
|
||||
│ └── faq/ # FAQ section
|
||||
├── navbar/ # Landing page navbar
|
||||
└── footer/ # Landing page footer
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
These components will be migrated from `marketing/` when beginning home page migration after Dashboard v2.
|
||||
|
||||
### Priority Sections to Migrate:
|
||||
1. **Hero** - Main landing with HeroFlame effect
|
||||
2. **Features** - Feature grid with animations
|
||||
3. **Testimonials** - Social proof section
|
||||
4. **Pricing** - Pricing tiers with heat buttons
|
||||
5. **FAQ** - Collapsible FAQ items
|
||||
|
||||
### Usage Pattern:
|
||||
```tsx
|
||||
// app/page.tsx (future)
|
||||
import { Hero } from '@/components/home/sections/hero';
|
||||
import { Features } from '@/components/home/sections/features';
|
||||
import { Testimonials } from '@/components/home/sections/testimonials';
|
||||
import { Pricing } from '@/components/home/sections/pricing';
|
||||
import { FAQ } from '@/components/home/sections/faq';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Features />
|
||||
<Testimonials />
|
||||
<Pricing />
|
||||
<FAQ />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
- **Fire theme**: Subtle flame effects in hero
|
||||
- **Performance**: Lazy load below-fold sections
|
||||
- **Responsive**: Mobile-first approach
|
||||
- **Animations**: Intersection observer for scroll effects
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { appConfig } from "@/config/app.config";
|
||||
|
||||
interface SidebarInputProps {
|
||||
onSubmit: (url: string, style: string, model: string, instructions?: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function SidebarInput({ onSubmit, disabled = false }: SidebarInputProps) {
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [selectedStyle, setSelectedStyle] = useState<string>("1");
|
||||
const [selectedModel, setSelectedModel] = useState<string>(appConfig.ai.defaultModel);
|
||||
const [additionalInstructions, setAdditionalInstructions] = useState<string>("");
|
||||
const [isValidUrl, setIsValidUrl] = useState<boolean>(false);
|
||||
|
||||
// Simple URL validation - currently unused but keeping for future use
|
||||
// const validateUrl = (urlString: string) => {
|
||||
// if (!urlString) return false;
|
||||
// const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
||||
// return urlPattern.test(urlString.toLowerCase());
|
||||
// };
|
||||
|
||||
const styles = [
|
||||
{ id: "1", name: "Glassmorphism", description: "Frosted glass effect" },
|
||||
{ id: "2", name: "Neumorphism", description: "Soft 3D shadows" },
|
||||
{ id: "3", name: "Brutalism", description: "Bold and raw" },
|
||||
{ id: "4", name: "Minimalist", description: "Clean and simple" },
|
||||
{ id: "5", name: "Dark Mode", description: "Dark theme design" },
|
||||
{ id: "6", name: "Gradient Rich", description: "Vibrant gradients" },
|
||||
{ id: "7", name: "3D Depth", description: "Dimensional layers" },
|
||||
{ id: "8", name: "Retro Wave", description: "80s inspired" },
|
||||
];
|
||||
|
||||
const models = appConfig.ai.availableModels.map(model => ({
|
||||
id: model,
|
||||
name: appConfig.ai.modelDisplayNames[model] || model,
|
||||
}));
|
||||
|
||||
const handleSubmit = (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!url.trim() || disabled) return;
|
||||
|
||||
onSubmit(url.trim(), selectedStyle, selectedModel, additionalInstructions || undefined);
|
||||
|
||||
// Reset form
|
||||
setUrl("");
|
||||
setAdditionalInstructions("");
|
||||
setIsValidUrl(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div >
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
{/* link to home page with button */}
|
||||
<Link href="/">
|
||||
<button className="w-full px-3 py-2 text-xs font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500">
|
||||
Generate a new website
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Options Section - Show when valid URL */}
|
||||
{isValidUrl && (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Style Selector */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-2">Style</label>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{styles.map((style) => (
|
||||
<button
|
||||
key={style.id}
|
||||
onClick={() => setSelectedStyle(style.id)}
|
||||
disabled={disabled}
|
||||
className={`
|
||||
py-2 px-2 rounded text-xs font-medium border transition-all text-center
|
||||
${selectedStyle === style.id
|
||||
? 'border-orange-500 bg-orange-50 text-orange-900'
|
||||
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700'
|
||||
}
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
{style.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selector */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-2">AI Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full px-3 py-2 text-xs font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Additional Instructions */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-2">Additional Instructions (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={additionalInstructions}
|
||||
onChange={(e) => setAdditionalInstructions(e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full px-3 py-2 text-xs text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400"
|
||||
placeholder="e.g., make it more colorful, add animations..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValidUrl || disabled}
|
||||
className={`
|
||||
w-full py-2.5 px-4 rounded-lg text-sm font-medium transition-all
|
||||
${isValidUrl && !disabled
|
||||
? 'bg-orange-500 hover:bg-orange-600 text-white'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{disabled ? 'Scraping...' : 'Scrape Site'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import HeroInputSubmitButton from "@/components/app/(home)/sections/hero-input/Button/Button";
|
||||
|
||||
interface SidebarQuickInputProps {
|
||||
onSubmit: (url: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function SidebarQuickInput({ onSubmit, disabled = false }: SidebarQuickInputProps) {
|
||||
const [url, setUrl] = useState<string>("");
|
||||
|
||||
const handleSubmit = (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!url.trim() || disabled) return;
|
||||
|
||||
onSubmit(url.trim());
|
||||
setUrl("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="p-3 flex items-center gap-3">
|
||||
<input
|
||||
className="flex-1 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none"
|
||||
placeholder="Enter a new URL to regenerate..."
|
||||
type="text"
|
||||
value={url}
|
||||
disabled={disabled}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div onClick={handleSubmit}>
|
||||
<HeroInputSubmitButton dirty={url.length > 0} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user