diff --git a/src/components/homepage/AnimatedButton.astro b/src/components/homepage/AnimatedButton.astro new file mode 100644 index 0000000..34fcbab --- /dev/null +++ b/src/components/homepage/AnimatedButton.astro @@ -0,0 +1,94 @@ +--- +const buttonText = 'Analyze URL'; +const buttonType = 'submit'; +--- + + + + + + diff --git a/src/components/homepage/AnimatedInput.astro b/src/components/homepage/AnimatedInput.astro new file mode 100644 index 0000000..d4488d6 --- /dev/null +++ b/src/components/homepage/AnimatedInput.astro @@ -0,0 +1,186 @@ +--- +const placeholders = [ + 'duck.com', + 'github.com', + 'google.com', + 'x.com', + 'bbc.co.uk', + 'wikipedia.org', + 'openai.com', +]; +--- + + +
+ +
+ + {placeholders.map((placeholder, index) => ( + + ))} +
+
+ + + + + diff --git a/src/components/homepage/HeroForm.astro b/src/components/homepage/HeroForm.astro new file mode 100644 index 0000000..f5d2e38 --- /dev/null +++ b/src/components/homepage/HeroForm.astro @@ -0,0 +1,128 @@ +--- +import AnimatedButton from "./AnimatedButton.astro" +import AnimatedInput from "./AnimatedInput.astro" +--- + +
+

+ Check Web + Web + Check +

+
+

We give you X-Ray
Vision for your Website

+

+ In just 20 seconds, you can see + what attackers already know +

+
+ + + + +
+
X
+
+ + + + diff --git a/src/components/homepage/HomeBackground.tsx b/src/components/homepage/HomeBackground.tsx new file mode 100644 index 0000000..7102072 --- /dev/null +++ b/src/components/homepage/HomeBackground.tsx @@ -0,0 +1,174 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import styled from '@emotion/styled'; + +// Global Animation Configuration Constants +const dotSpacing = 32; // Number of px between each dot +const meteorCount = 4; // Number of meteors to display at any given time +const tailLength = 80; // Length of the meteor tail in px +const distanceBase = 5; // Base distance for meteor to travel in grid units +const distanceVariance = 5; // Variance for randomization to append to travel in grid units +const durationBase = 1.5; // Base duration for meteor to travel in seconds +const durationVariance = 1; // Variance for randomization to append to travel in seconds +const delayBase = 500; // Base delay for meteor to respawn in milliseconds +const delayVariance = 1500; // Variance for randomization to append to respawn in milliseconds +const tailDuration = 0.25; // Duration for meteor tail to retract in seconds +const headEasing = [0.8, 0.6, 1, 1]; // Easing for meteor head +const tailEasing = [0.5, 0.6, 0.6, 1]; // Easing for meteor tail + +const MeteorContainer = styled(motion.div)` + position: absolute; + width: 4px; + height: 4px; + border-radius: 50%; + background-color: #9fef00; + top: 1px; +`; + +const Tail = styled(motion.div)` + position: absolute; + top: -80px; + left: 1px; + width: 2px; + height: 80px; + background: linear-gradient(to bottom, transparent, #9fef00); +`; + +const StyledSvg = styled.svg` + pointer-events: none; + position: absolute; + inset: 0; + height: 100%; + width: 100%; + fill: rgba(100, 100, 100, 0.5); + defs pattern circle { + z-index: 1; + } +`; + +const StyledRect = styled.rect` + width: 100%; + height: 100%; + stroke-width: 0; +`; + +const Container = styled.div` + pointer-events: none; + position: absolute; + height: 100vh; + width: 100vw; + z-index: 1; + top: 0; + left: 0; + background: radial-gradient(circle at center top, transparent, transparent 60%, var(--background) 100%); +`; + +const generateMeteor = (id: number, gridSizeX: number, gridSizeY: number) => { + const column = Math.floor(Math.random() * gridSizeX); + const startRow = Math.floor(Math.random() * (gridSizeY - 12)); + const travelDistance = distanceBase + Math.floor(Math.random() * distanceVariance); + const duration = durationBase + Math.floor(Math.random() * durationVariance); + + return { + id, + column, + startRow, + endRow: startRow + travelDistance, + duration, + tailVisible: true, + animationStage: 'traveling', + opacity: 1, // Initial opacity + }; +}; + +const generateInitialMeteors = (gridSizeX: number, gridSizeY: number) => { + const seen = new Set(); + return Array.from({ length: meteorCount }, (_, index) => generateMeteor(index, gridSizeX, gridSizeY)) + .filter(item => !seen.has(item.column) && seen.add(item.column)); +}; + +const WebCheckHomeBackground = () => { + const [gridSizeX, setGridSizeX] = useState(Math.floor(window.innerWidth / dotSpacing)); + const [gridSizeY, setGridSizeY] = useState(Math.floor(window.innerHeight / dotSpacing)); + const [meteors, setMeteors] = useState(() => generateInitialMeteors(gridSizeX, gridSizeY)); + + const handleAnimationComplete = (id: number) => { + setMeteors(current => + current.map(meteor => { + if (meteor.id === id) { + if (meteor.animationStage === 'traveling') { + // Transition to retracting tail + return { ...meteor, tailVisible: false, animationStage: 'retractingTail' }; + } else if (meteor.animationStage === 'retractingTail') { + // Set to resetting and make invisible + return { ...meteor, animationStage: 'resetting', opacity: 0 }; + } else if (meteor.animationStage === 'resetting') { + // Respawn the meteor after a delay + setTimeout(() => { + setMeteors(current => + current.map(m => m.id === id ? generateMeteor(id, gridSizeX, gridSizeY) : m) + ); + }, delayBase + Math.random() * delayVariance); + } + } + return meteor; + }) + ); + }; + + useEffect(() => { + const handleResize = () => { + setGridSizeX(Math.floor(window.innerWidth / dotSpacing)); + setGridSizeY(Math.floor(window.innerHeight / dotSpacing)); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return ( + <> + + + + + + + + + + + {meteors.map(({ id, column, startRow, endRow, duration, tailVisible, animationStage, opacity }) => { + return ( + handleAnimationComplete(id)} + > + + + ); + })} + + ); +}; + +export default WebCheckHomeBackground;