diff --git a/src/components/landing/ChooseYourPath.astro b/src/components/landing/ChooseYourPath.astro new file mode 100644 index 0000000..eed159c --- /dev/null +++ b/src/components/landing/ChooseYourPath.astro @@ -0,0 +1,55 @@ +--- +import PathCard from '../ui/PathCard.astro'; +import { ROUTES, EXTERNAL_LINKS } from '../../data/landingContent'; +import { getTotalLibraryCount } from '../../utils/landingData'; +--- + +
+
+

+ One Platform, Two Powerful Workflows +

+ +

+ Whether you code solo or collaborate with a team, we've got you covered +

+ +
+ + + + + +
+
+
diff --git a/src/components/landing/Community.astro b/src/components/landing/Community.astro new file mode 100644 index 0000000..3ab3d03 --- /dev/null +++ b/src/components/landing/Community.astro @@ -0,0 +1,60 @@ +--- +import { getContributors, getContributorCount } from '../../utils/landingData'; +import { EXTERNAL_LINKS } from '../../data/landingContent'; + +const contributors = getContributors(); +const contributorCount = getContributorCount(); +--- + +
+
+

Built by 10xDevs & Friends

+ +

+ Open source project with {contributorCount}+ contributors and growing +

+ + +
+ { + contributors.map((contributor) => ( + + {contributor.name} + + )) + } +
+ + +
+
diff --git a/src/components/landing/FeatureCard.astro b/src/components/landing/FeatureCard.astro new file mode 100644 index 0000000..3bf434a --- /dev/null +++ b/src/components/landing/FeatureCard.astro @@ -0,0 +1,25 @@ +--- +interface Props { + icon: string; + title: string; + description: string; +} + +const { icon, title, description } = Astro.props; +--- + +
+
+ {icon} +
+ +

+ {title} +

+ +

+ {description} +

+
diff --git a/src/components/landing/FeaturesGrid.astro b/src/components/landing/FeaturesGrid.astro new file mode 100644 index 0000000..9a3f275 --- /dev/null +++ b/src/components/landing/FeaturesGrid.astro @@ -0,0 +1,173 @@ +--- +import { GRADIENT_ORB_DELAYS } from '../../data/landingContent'; +import { getTotalLibraryCount } from '../../utils/landingData'; + +// Features - Rules Builder +const RULES_BUILDER_FEATURES = [ + { + icon: '🎨', + title: 'Visual Rule Builder', + description: 'Intuitive drag-and-drop interface for creating AI rules without writing code', + }, + { + icon: 'πŸ“š', + title: `${getTotalLibraryCount()}+ Frameworks & Libraries`, + description: + 'Comprehensive coverage across 6 technology layers: frontend, backend, database, DevOps, testing, and coding practices', + }, + { + icon: 'πŸ“¦', + title: 'Smart Import', + description: + 'Automatically detect your tech stack by dropping package.json or requirements.txt files', + }, + { + icon: 'πŸ’Ύ', + title: 'Flexible Export', + description: 'Export as single-file or multi-file markdown optimized for different AI editors', + }, + { + icon: 'πŸ—‚οΈ', + title: 'Personal Collections', + description: 'Save and manage reusable rule sets for different projects and contexts', + }, +]; + +// Features - Prompt Library +const PROMPT_LIBRARY_FEATURES = [ + { + icon: '🏒', + title: 'Organization Management', + description: + 'Multi-organization support with role-based access control (RBAC) for admins and members', + }, + { + icon: '✍️', + title: 'Content Curation', + description: 'Draft/publish workflow ensures quality and consistency across team prompts', + }, + { + icon: 'πŸ‘₯', + title: 'Member Experience', + description: "Browse, filter, and copy prompts from your organization's centralized library", + }, + { + icon: '🀝', + title: 'Collaboration Tools', + description: 'Centralized repository for team knowledge sharing and prompt standardization', + }, +]; + +// Features - Universal +const UNIVERSAL_FEATURES = [ + { + icon: 'πŸ”Œ', + title: 'MCP Server Integration', + description: + 'Programmatic access to both Rules Builder and Prompt Library via Model Context Protocol for AI assistants', + }, +]; +--- + +
+ +
+ + +
+ + +
+
+
+
+
+
+ +
+

+ Powerful Features for Everyone +

+

+ Build personal AI rules and manage team prompts in one platform +

+ + +
+ +
+ +
+
πŸ§‘β€πŸ’»
+

Rules Builder

+

For individual developers

+
+ + +
+ { + RULES_BUILDER_FEATURES.map((feature) => ( +
+
{feature.icon}
+
+

{feature.title}

+

{feature.description}

+
+
+ )) + } +
+
+ + +
+ +
+
πŸ‘₯
+

Prompt Library

+

For development teams

+
+ + +
+ { + PROMPT_LIBRARY_FEATURES.map((feature) => ( +
+
{feature.icon}
+
+

{feature.title}

+

{feature.description}

+
+
+ )) + } +
+
+
+ + + { + UNIVERSAL_FEATURES.map((feature) => ( +
+
+
{feature.icon}
+
+

{feature.title}

+

{feature.description}

+
+
+
+ )) + } +
+
diff --git a/src/components/landing/FinalCTA.astro b/src/components/landing/FinalCTA.astro new file mode 100644 index 0000000..df974fe --- /dev/null +++ b/src/components/landing/FinalCTA.astro @@ -0,0 +1,46 @@ +--- +import Button from '../ui/Button.astro'; +import { GRADIENT_ORB_DELAYS, ROUTES, EXTERNAL_LINKS } from '../../data/landingContent'; +--- + +
+ +
+
+ + +
+
+
+
+
+
+ + +
+ +
+

+ Ready to Supercharge Your AI Coding? +

+ +

+ Join developers using 10xRules.ai to get better AI suggestions +

+ +
+ + +
+
+
diff --git a/src/components/landing/Hero.astro b/src/components/landing/Hero.astro new file mode 100644 index 0000000..b790938 --- /dev/null +++ b/src/components/landing/Hero.astro @@ -0,0 +1,140 @@ +--- +import Button from '../ui/Button.astro'; +import { GRADIENT_ORB_DELAYS, ROUTES, ANCHORS } from '../../data/landingContent'; +import { getTotalLibraryCount } from '../../utils/landingData'; + +// Editor compatibility list +const EDITOR_COMPATIBILITY = [ + { name: 'GitHub Copilot', icon: 'πŸ™' }, + { name: 'Cursor', icon: 'πŸ–±οΈ' }, + { name: 'Windsurf', icon: 'πŸ„' }, + { name: 'Claude Code', icon: 'πŸ€–' }, +]; + +const totalFrameworks = getTotalLibraryCount(); +--- + +
+ +
+
+
+
+
+
+
+
+ + +
+ +
+
+ +
+ Trusted by 10xDevs community +
+ + +

+ + AI Coding Rules + + & Team Prompts + in One Platform +

+ + +

+ Build personal AI rules from {totalFrameworks}+ frameworks,
manage shared prompts with your team. +

+ + +
+ + +
+ + +
+

Compatible with:

+
+ { + EDITOR_COMPATIBILITY.map((editor) => ( +
+ {editor.icon} + {editor.name} +
+ )) + } +
+
+ + +
+
+
+ {totalFrameworks}+ +
+
Frameworks
+
+
+
+
+ Team +
+
Collaboration
+
+
+
+
+ Open +
+
Source
+
+
+ + +
+
+ 10xRules.ai Rules Builder Demo +
+
+
+
+
diff --git a/src/components/landing/HowItWorks.astro b/src/components/landing/HowItWorks.astro new file mode 100644 index 0000000..47baa21 --- /dev/null +++ b/src/components/landing/HowItWorks.astro @@ -0,0 +1,197 @@ +--- +import StepCard from '../ui/StepCard.astro'; +import { ANIMATION_DURATIONS } from '../../data/landingAnimations'; +import { getTotalLibraryCount } from '../../utils/landingData'; + +// How It Works - Rules Builder steps +const RULES_BUILDER_STEPS = [ + { + stepNumber: 1, + title: 'Select Your Stack', + description: `Choose from ${getTotalLibraryCount()}+ frameworks across 6 technology layers`, + icon: 'πŸ”', + }, + { + stepNumber: 2, + title: 'Customize & Import', + description: 'Configure rules manually or drop package.json/requirements.txt for smart import', + icon: 'βš™οΈ', + }, + { + stepNumber: 3, + title: 'Export Anywhere', + description: 'Download as markdown or copy to clipboard for use in any AI-powered editor', + icon: 'πŸ“₯', + }, +]; + +// How It Works - Prompt Library steps +const PROMPT_LIBRARY_STEPS = [ + { + stepNumber: 1, + title: 'Organize Content', + description: 'Admins create collections and segments to structure team knowledge', + icon: 'πŸ“', + }, + { + stepNumber: 2, + title: 'Curate & Publish', + description: 'Draft, review, and publish prompts with approval workflow', + icon: '✍️', + }, + { + stepNumber: 3, + title: 'Team Access', + description: 'Members browse, filter, and copy curated prompts for consistent AI interactions', + icon: 'πŸ‘₯', + }, +]; +--- + +
+
+

How It Works

+ +
+ +
+ + +
+ + +
+
+ { + RULES_BUILDER_STEPS.map((step, index) => ( +
+ +
+ )) + } +
+
+ + + +
+
+
+ + diff --git a/src/components/landing/Landing.astro b/src/components/landing/Landing.astro new file mode 100644 index 0000000..9e3d621 --- /dev/null +++ b/src/components/landing/Landing.astro @@ -0,0 +1,64 @@ +--- +// Main landing page container +// Imports all section components and stacks them vertically +import LandingHeader from './LandingHeader.astro'; +import Hero from './Hero.astro'; +import ProblemSolution from './ProblemSolution.astro'; +import HowItWorks from './HowItWorks.astro'; +import FeaturesGrid from './FeaturesGrid.astro'; +import TechStackShowcase from './TechStackShowcase.astro'; +import MCPIntegration from './MCPIntegration.astro'; +import ChooseYourPath from './ChooseYourPath.astro'; +import Community from './Community.astro'; +import FinalCTA from './FinalCTA.astro'; +import { ANCHORS } from '../../data/landingContent'; +--- + +
+ + + Skip to main content + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + diff --git a/src/components/landing/LandingHeader.astro b/src/components/landing/LandingHeader.astro new file mode 100644 index 0000000..b83b3a8 --- /dev/null +++ b/src/components/landing/LandingHeader.astro @@ -0,0 +1,63 @@ +--- +import Button from '../ui/Button.astro'; +import { ROUTES, ANCHORS, EXTERNAL_LINKS } from '../../data/landingContent'; +--- + + diff --git a/src/components/landing/MCPIntegration.astro b/src/components/landing/MCPIntegration.astro new file mode 100644 index 0000000..09cf4bc --- /dev/null +++ b/src/components/landing/MCPIntegration.astro @@ -0,0 +1,74 @@ +--- +// MCP Server Integration section +import { GRADIENT_ORB_DELAYS } from '../../data/landingContent'; +--- + +
+ +
+ + +
+ + +
+
+
+
+ +
+

+ Programmatic Access via MCP Server +

+ +

+ Connect your AI assistants directly to both Rules Builder and Prompt Library using the Model + Context Protocol (MCP) +

+ +
+ +
+

Access Rules Builder

+

+ Fetch frameworks and content programmatically for your AI workflow +

+
    +
  • + βœ“ + List available rule categories +
  • +
  • + βœ“ + Retrieve specific rule content +
  • +
+
+ + +
+

Access Prompt Library

+

Retrieve organization prompts for team-wide consistency

+
    +
  • + βœ“ + Browse organization collections +
  • +
  • + βœ“ + Fetch curated prompts +
  • +
+
+
+ +
+

+ Compatible with Cursor, Claude Desktop, and other MCP-enabled editors +

+
+
+
diff --git a/src/components/landing/ProblemSolution.astro b/src/components/landing/ProblemSolution.astro new file mode 100644 index 0000000..6a7d569 --- /dev/null +++ b/src/components/landing/ProblemSolution.astro @@ -0,0 +1,177 @@ +--- +// Problem/Solution section configuration +const PROBLEM_SOLUTION_CONFIG = { + headline: 'Stop Fighting Your AI Coding Assistant', + subheadline: + 'Generic AI suggestions waste your time. Get contextual, framework-specific help instead.', + + stories: [ + { + emoji: 'πŸ§‘β€πŸ’»', + title: 'For Individual Developers', + problem: { + badge: 'BEFORE', + emoji: '😀', + title: 'The Frustration', + items: [ + '"Use useState" when you\'re building with Vue Composition API', + 'Generic Express.js patterns when you need Fastify best practices', + "Wasting time correcting AI that doesn't know your stack", + ], + }, + solution: { + badge: 'AFTER', + emoji: 'πŸš€', + title: 'Your AI, Your Way', + items: [ + 'AI knows you use Vue 3, Pinia, and TypeScript strict mode', + 'Suggestions match your exact tech stack from day one', + 'Import your package.json and get instant context', + ], + }, + }, + { + emoji: 'πŸ‘₯', + title: 'For Development Teams', + problem: { + badge: 'BEFORE', + emoji: 'πŸ˜΅β€πŸ’«', + title: 'The Chaos', + items: [ + 'Everyone has different prompts in random Notion docs', + 'Junior devs reinvent the wheel, seniors copy-paste from Slack', + 'Code reviews catch AI-generated antipatterns too late', + ], + }, + solution: { + badge: 'AFTER', + emoji: '🎯', + title: 'One Source of Truth', + items: [ + 'Centralized prompt library with approval workflows', + 'Junior devs get best practices, seniors maintain standards', + 'Consistent AI output across the entire team', + ], + }, + }, + ], +} as const; + +const { headline, subheadline, stories } = PROBLEM_SOLUTION_CONFIG; + +// Helper to determine card styling based on type and index +const getCardStyles = (type: 'problem' | 'solution', storyIndex: number) => { + if (type === 'problem') { + // First story uses red, second uses orange + const colorScheme = + storyIndex === 0 + ? { + gradient: 'from-red-900/20', + border: 'border-red-900/50', + badge: 'bg-red-500', + titleColor: 'text-red-400', + } + : { + gradient: 'from-orange-900/20', + border: 'border-orange-900/50', + badge: 'bg-orange-500', + titleColor: 'text-orange-400', + }; + return { + cardClass: `problem-card before-after-card relative p-8 bg-gradient-to-br ${colorScheme.gradient} to-gray-900 border-2 ${colorScheme.border} rounded-xl`, + badgeClass: `absolute -top-3 -left-3 ${colorScheme.badge} text-white text-xs font-bold px-3 py-1 rounded-full`, + titleClass: `text-xl font-bold ${colorScheme.titleColor} mb-4`, + iconColor: storyIndex === 0 ? 'text-red-500' : 'text-orange-500', + iconSymbol: 'Γ—', + }; + } else { + // First story uses blue-teal, second uses purple-teal + const colorScheme = + storyIndex === 0 + ? { + gradient: 'from-blue-900/20 via-teal-900/20', + badge: 'from-blue-500 to-teal-500', + titleGradient: 'from-blue-400 to-teal-400', + shadow: 'shadow-teal-500/10', + } + : { + gradient: 'from-purple-900/20 via-teal-900/20', + badge: 'from-purple-500 to-teal-500', + titleGradient: 'from-purple-400 to-teal-400', + shadow: 'shadow-purple-500/10', + }; + return { + cardClass: `solution-card before-after-card relative p-8 bg-gradient-to-br ${colorScheme.gradient} to-gray-900 border-2 border-teal-500/50 rounded-xl shadow-lg ${colorScheme.shadow}`, + badgeClass: `absolute -top-3 -left-3 bg-gradient-to-r ${colorScheme.badge} text-white text-xs font-bold px-3 py-1 rounded-full`, + titleClass: `text-xl font-bold bg-gradient-to-r ${colorScheme.titleGradient} bg-clip-text text-transparent mb-4`, + iconColor: 'text-teal-500', + iconSymbol: 'βœ“', + }; + } +}; +--- + +
+ +
+ +
+ +
+

+ {headline} +

+

+ {subheadline} +

+
+ + + { + stories.map((story, storyIndex) => { + const problemStyles = getCardStyles('problem', storyIndex); + const solutionStyles = getCardStyles('solution', storyIndex); + const isLastStory = storyIndex === stories.length - 1; + + return ( +
+
+
{story.emoji}
+

{story.title}

+
+ +
+
+
{story.problem.badge}
+
{story.problem.emoji}
+

{story.problem.title}

+
+ {story.problem.items.map((item) => ( +

+ {problemStyles.iconSymbol} + {item} +

+ ))} +
+
+ +
+
{story.solution.badge}
+
{story.solution.emoji}
+

{story.solution.title}

+
+ {story.solution.items.map((item) => ( +

+ {solutionStyles.iconSymbol} + {item} +

+ ))} +
+
+
+
+ ); + }) + } +
+
diff --git a/src/components/landing/TechStackShowcase.astro b/src/components/landing/TechStackShowcase.astro new file mode 100644 index 0000000..0803e30 --- /dev/null +++ b/src/components/landing/TechStackShowcase.astro @@ -0,0 +1,49 @@ +--- +import { getLayerStatistics, getTotalLibraryCount } from '../../utils/landingData'; +import { GRADIENT_ORB_DELAYS } from '../../data/landingContent'; + +const layerStats = getLayerStatistics(); +const totalCount = getTotalLibraryCount(); +--- + +
+ +
+
+
+
+
+
+ +
+

+ Comprehensive Tech Stack Coverage +

+ +
+ { + layerStats.map((stat) => ( +
+
{stat.icon}
+
+ {stat.count} +
+
{stat.name}
+
+ )) + } +
+ +
+

+ {totalCount}+ Total Frameworks +

+
+
+
diff --git a/src/components/ui/Button.astro b/src/components/ui/Button.astro new file mode 100644 index 0000000..886d516 --- /dev/null +++ b/src/components/ui/Button.astro @@ -0,0 +1,25 @@ +--- +interface Props { + variant?: 'primary' | 'secondary'; + href: string; + class?: string; +} + +const { variant = 'primary', href, class: className = '' } = Astro.props; + +const baseStyles = + 'inline-flex items-center justify-center px-6 py-3 rounded-lg font-medium transition-all duration-300 shadow-lg hover:shadow-xl transform '; + +const variantStyles = { + primary: + 'bg-gradient-to-r from-blue-500 to-teal-500 hover:from-teal-500 hover:to-purple-500 text-white', + secondary: + 'border-2 border-gray-700 hover:border-gray-600 bg-gray-900 hover:bg-gray-800 text-white', +}; + +const classes = `${baseStyles} ${variantStyles[variant]} ${className}`; +--- + + + + diff --git a/src/components/ui/PathCard.astro b/src/components/ui/PathCard.astro new file mode 100644 index 0000000..6939c03 --- /dev/null +++ b/src/components/ui/PathCard.astro @@ -0,0 +1,54 @@ +--- +import Button from './Button.astro'; + +interface Props { + icon: string; + title: string; + subtitle: string; + features: string[]; + ctaText: string; + ctaHref: string; + note?: string; +} + +const { icon, title, subtitle, features, ctaText, ctaHref, note } = Astro.props; +--- + +
+ +
+ {icon} +
+ + +

+ {title} +

+ +

+ {subtitle} +

+ + + + + +
+ + + {note &&

{note}

} +
+
diff --git a/src/components/ui/StepCard.astro b/src/components/ui/StepCard.astro new file mode 100644 index 0000000..7aea253 --- /dev/null +++ b/src/components/ui/StepCard.astro @@ -0,0 +1,35 @@ +--- +interface Props { + stepNumber: number; + title: string; + description: string; + icon: string; +} + +const { stepNumber, title, description, icon } = Astro.props; +--- + +
+ +
+ {stepNumber} +
+ + +
+ {icon} +
+ + +

+ {title} +

+ +

+ {description} +

+
diff --git a/src/data/landingAnimations.ts b/src/data/landingAnimations.ts new file mode 100644 index 0000000..0108518 --- /dev/null +++ b/src/data/landingAnimations.ts @@ -0,0 +1,43 @@ +/** + * Landing page animation constants + * Animation-related configuration shared across landing page scripts + */ + +// Animation durations (in milliseconds) +export const ANIMATION_DURATIONS = { + // Transition durations for hover effects + HOVER_TRANSITION: 300, + + // Tab switching animations + TAB_FADE_IN: 400, + TAB_FADE_OUT: 200, + TAB_HIDE_DELAY: 200, + + // Counter animations + COUNTER_DEFAULT: 1500, + COUNTER_LAYER: 1200, + COUNTER_STAGGER_DELAY: 100, + + // Copy button feedback + COPY_FEEDBACK: 2000, +} as const; + +// Root margins for Intersection Observer (in pixels) +export const ROOT_MARGINS = { + DEFAULT: '0px 0px -50px 0px', + LARGE: '0px 0px -100px 0px', +} as const; + +// Threshold values for Intersection Observer (0-1 scale) +export const OBSERVER_THRESHOLDS = { + LOW: 0.1, + MEDIUM: 0.2, + HIGH: 0.3, +} as const; + +// Magnetic button effect settings +export const MAGNETIC_BUTTON = { + TRANSFORM_MULTIPLIER: 0.2, + HOVER_SCALE: 1.05, + DEFAULT_SCALE: 1, +} as const; diff --git a/src/data/landingContent.ts b/src/data/landingContent.ts new file mode 100644 index 0000000..e154c7b --- /dev/null +++ b/src/data/landingContent.ts @@ -0,0 +1,61 @@ +/** + * Landing page shared constants + * Contains truly shared content used across multiple landing page components + * Component-specific content has been moved to respective component files + * Animation constants moved to landingAnimations.ts + */ + +// GitHub repository URL +export const GITHUB_REPO_URL = 'https://github.com/przeprogramowani/ai-rules-builder'; + +// MCP documentation URL +export const MCP_DOCS_URL = + 'https://github.com/przeprogramowani/ai-rules-builder/tree/main/mcp-server'; + +// ============================================================================ +// LINKS AND ROUTES +// ============================================================================ + +// Internal application routes +export const ROUTES = { + HOME: '/', + LOGIN: '/auth/login', + SIGNUP: '/auth/signup', +} as const; + +// Anchor links for in-page navigation +export const ANCHORS = { + MAIN_CONTENT: '#main-content', + FEATURES: '#features', + CHOOSE_YOUR_PATH: '#choose-your-path', +} as const; + +// External links +export const EXTERNAL_LINKS = { + GITHUB: GITHUB_REPO_URL, + GITHUB_CONTRIBUTING: `${GITHUB_REPO_URL}/blob/main/CONTRIBUTING.md`, + MCP_DOCS: MCP_DOCS_URL, + LIBRARY_FORM: 'https://airtable.com/appBN64leXIbQ1gDe/pagwa0kilsbzLUFBQ/form', +} as const; + +// ============================================================================ +// ANIMATION CONSTANTS +// ============================================================================ + +// Animation delays for gradient orbs (in seconds) +export const GRADIENT_ORB_DELAYS = { + HERO_BLUE: '0s', + HERO_TEAL: '7s', + HERO_PURPLE: '3s', + + FEATURES_BLUE: '2s', + FEATURES_TEAL: '5s', + + TECH_PURPLE: '4s', + TECH_TEAL: '1s', + + MCP_PURPLE: '6s', + + CTA_BLUE: '0s', + CTA_TEAL: '3s', +} as const; diff --git a/src/layouts/partials/SEO.astro b/src/layouts/partials/SEO.astro index 320e3cc..bf31b82 100644 --- a/src/layouts/partials/SEO.astro +++ b/src/layouts/partials/SEO.astro @@ -1,13 +1,16 @@ --- const siteUrl = 'https://10xrules.ai'; const siteDescription = - 'Create and manage rules for best-in-class AI tools like GitHub Copilot, Cursor & Windsurf. Make AI Agents aware of your preferences and coding style.'; -const siteTitle = '10xRules.ai'; + 'Build personal AI coding rules from 100+ frameworks and manage team prompts with your organization on one platform. Compatible with GitHub Copilot, Cursor & Windsurf.'; +const siteTitle = '10xRules.ai - AI Coding Rules & Team Prompts'; +const keywords = + 'AI coding rules, prompt library, team collaboration, GitHub Copilot, Cursor, Windsurf, MCP server, developer tools, coding frameworks, prompt management'; --- + @@ -33,3 +36,40 @@ const siteTitle = '10xRules.ai'; + + + diff --git a/src/pages/index.astro b/src/pages/index.astro index f3b32c0..9ad7064 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,17 +3,23 @@ import Layout from '../layouts/Layout.astro'; import Topbar from '../components/Topbar'; import TwoPane from '../components/TwoPane'; import Footer from '../components/Footer'; +import Landing from '../components/landing/Landing.astro'; const user = Astro.locals.user; const initialUrl = Astro.url; --- -
- -
- -
-
-
+ {!user && } + { + user && ( +
+ +
+ +
+
+ ) + }
diff --git a/src/scripts/landing-animations.ts b/src/scripts/landing-animations.ts new file mode 100644 index 0000000..f07468c --- /dev/null +++ b/src/scripts/landing-animations.ts @@ -0,0 +1,82 @@ +/** + * Landing page scroll animations + * Uses Intersection Observer for fade-in effects + */ + +import { OBSERVER_THRESHOLDS, ROOT_MARGINS } from '../data/landingAnimations'; + +// Initialize scroll animations when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + // Detect Safari and mobile devices for performance optimizations + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || + (navigator.maxTouchPoints > 0 && window.innerWidth < 1024); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + // Add classes for CSS targeting + if (isSafari) { + document.documentElement.classList.add('is-safari'); + } + if (isMobile) { + document.documentElement.classList.add('is-mobile'); + } + if (prefersReducedMotion) { + document.documentElement.classList.add('prefers-reduced-motion'); + } + + // For Safari and mobile, disable expensive animations entirely + const shouldDisableAnimations = isSafari || isMobile || prefersReducedMotion; + if (shouldDisableAnimations) { + console.log('[Performance] Disabling expensive animations for better performance', { + isSafari, + isMobile, + prefersReducedMotion, + }); + } + + // Only enable scroll animations on high-performance browsers (Chrome Desktop) + if (!shouldDisableAnimations) { + // Create intersection observer for fade-in animations + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('animate-fade-in'); + // Optionally unobserve after animation + observer.unobserve(entry.target); + } + }); + }, + { + threshold: OBSERVER_THRESHOLDS.LOW, + rootMargin: ROOT_MARGINS.DEFAULT, + }, + ); + + // Observe all sections for fade-in + const sections = document.querySelectorAll('section'); + sections.forEach((section) => { + section.classList.add('animate-on-scroll'); + observer.observe(section); + }); + } + + // Smooth scroll for anchor links + const anchorLinks = document.querySelectorAll('a[href^="#"]'); + anchorLinks.forEach((link) => { + link.addEventListener('click', (e) => { + const href = link.getAttribute('href'); + if (href && href !== '#') { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + } + }); + }); +}); diff --git a/src/scripts/landing-counters.ts b/src/scripts/landing-counters.ts new file mode 100644 index 0000000..4739b53 --- /dev/null +++ b/src/scripts/landing-counters.ts @@ -0,0 +1,171 @@ +/** + * Number counter animations for landing page + * Animates numbers from 0 to target value with smooth transitions + */ + +import { ANIMATION_DURATIONS, OBSERVER_THRESHOLDS, ROOT_MARGINS } from '../data/landingAnimations'; + +interface CounterOptions { + duration?: number; + startDelay?: number; + easing?: (t: number) => number; +} + +/** + * Easing function for smooth counter animation + */ +const easeOutQuad = (t: number): number => { + return t * (2 - t); +}; + +/** + * Animate a number counter from 0 to target value + */ +export function animateCounter( + element: HTMLElement, + target: number, + options: CounterOptions = {}, +): void { + const { + duration = ANIMATION_DURATIONS.COUNTER_DEFAULT, + startDelay = 0, + easing = easeOutQuad, + } = options; + + const suffix = element.dataset.suffix || ''; + const prefix = element.dataset.prefix || ''; + + const startTime = performance.now() + startDelay; + + function update(currentTime: number) { + const elapsed = currentTime - startTime; + + if (elapsed < 0) { + requestAnimationFrame(update); + return; + } + + const progress = Math.min(elapsed / duration, 1); + const easedProgress = easing(progress); + const currentValue = Math.floor(easedProgress * target); + + element.textContent = `${prefix}${currentValue}${suffix}`; + + if (progress < 1) { + requestAnimationFrame(update); + } else { + // Ensure final value is exact + element.textContent = `${prefix}${target}${suffix}`; + } + } + + requestAnimationFrame(update); +} + +/** + * Initialize counter animations when elements come into view + */ +export function initializeCounters(): void { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && !entry.target.classList.contains('counted')) { + entry.target.classList.add('counted'); + const element = entry.target as HTMLElement; + const target = parseInt(element.dataset.count || '0', 10); + const delay = parseInt(element.dataset.delay || '0', 10); + + animateCounter(element, target, { + duration: ANIMATION_DURATIONS.COUNTER_DEFAULT, + startDelay: delay, + }); + + // Unobserve after animation starts + observer.unobserve(entry.target); + } + }); + }, + { + threshold: OBSERVER_THRESHOLDS.MEDIUM, + rootMargin: ROOT_MARGINS.LARGE, + }, + ); + + // Find all elements with data-count attribute + const counters = document.querySelectorAll('[data-count]'); + counters.forEach((counter) => observer.observe(counter)); +} + +/** + * Initialize staggered counter animations for layer cards + */ +export function initializeLayerCounters(): void { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const layerCounts = entry.target.querySelectorAll('.layer-count'); + layerCounts.forEach((counter, index) => { + if (!counter.classList.contains('counted')) { + counter.classList.add('counted'); + const element = counter as HTMLElement; + const target = parseInt(element.dataset.count || '0', 10); + + animateCounter(element, target, { + duration: ANIMATION_DURATIONS.COUNTER_LAYER, + startDelay: index * ANIMATION_DURATIONS.COUNTER_STAGGER_DELAY, + }); + } + }); + + observer.unobserve(entry.target); + } + }); + }, + { + threshold: OBSERVER_THRESHOLDS.MEDIUM, + }, + ); + + // Observe the tech stack container + const techStackSection = document.querySelector('#tech-stack'); + if (techStackSection) { + observer.observe(techStackSection); + } +} + +// Auto-initialize when DOM is ready +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', () => { + // Check if we should disable animations for performance + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || + (navigator.maxTouchPoints > 0 && window.innerWidth < 1024); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const shouldDisableAnimations = isSafari || isMobile || prefersReducedMotion; + + if (shouldDisableAnimations) { + // Show final values immediately without animation for better performance + const counters = document.querySelectorAll('[data-count]'); + counters.forEach((counter) => { + const element = counter as HTMLElement; + const target = element.dataset.count || '0'; + const suffix = element.dataset.suffix || ''; + const prefix = element.dataset.prefix || ''; + element.textContent = `${prefix}${target}${suffix}`; + }); + + const layerCounts = document.querySelectorAll('.layer-count'); + layerCounts.forEach((counter) => { + const element = counter as HTMLElement; + const target = element.dataset.count || '0'; + element.textContent = target; + }); + } else { + // Enable smooth counter animations on high-performance browsers + initializeCounters(); + initializeLayerCounters(); + } + }); +} diff --git a/src/scripts/landing-micro-interactions.ts b/src/scripts/landing-micro-interactions.ts new file mode 100644 index 0000000..2199120 --- /dev/null +++ b/src/scripts/landing-micro-interactions.ts @@ -0,0 +1,244 @@ +/** + * Micro-interactions for landing page + * Handles hover effects, tab switching animations, and other interactive behaviors + */ + +import { + ANIMATION_DURATIONS, + MAGNETIC_BUTTON, + OBSERVER_THRESHOLDS, + ROOT_MARGINS, +} from '../data/landingAnimations'; + +/** + * Enhance tab switching with fade transitions + */ +export function initializeTabAnimations(): void { + const tabButtons = document.querySelectorAll('[data-tab]'); + const tabContents = document.querySelectorAll('[data-tab-content]'); + + tabButtons.forEach((button) => { + button.addEventListener('click', () => { + const tabName = button.getAttribute('data-tab'); + + tabContents.forEach((content) => { + const contentId = content.getAttribute('data-tab-content'); + + if (contentId === tabName) { + content.classList.remove('hidden'); + (content as HTMLElement).style.animation = + `fadeIn ${ANIMATION_DURATIONS.TAB_FADE_IN}ms ease-out`; + } else { + (content as HTMLElement).style.animation = + `fadeOut ${ANIMATION_DURATIONS.TAB_FADE_OUT}ms ease-out`; + setTimeout(() => { + content.classList.add('hidden'); + }, ANIMATION_DURATIONS.TAB_HIDE_DELAY); + } + }); + }); + }); +} + +/** + * Initialize copy button interactions + */ +export function initializeCopyButtons(): void { + const copyButtons = document.querySelectorAll('.copy-button'); + + copyButtons.forEach((button) => { + button.addEventListener('click', async () => { + const targetId = button.getAttribute('data-copy-target'); + if (!targetId) return; + + const target = document.querySelector(targetId); + if (!target) return; + + const text = target.textContent || ''; + + try { + await navigator.clipboard.writeText(text); + + // Add success animation + button.classList.add('copied'); + + // Change button text temporarily + const originalText = button.textContent; + button.textContent = 'Copied!'; + + setTimeout(() => { + button.classList.remove('copied'); + button.textContent = originalText; + }, ANIMATION_DURATIONS.COPY_FEEDBACK); + } catch (err) { + console.error('Failed to copy text:', err); + } + }); + }); +} + +/** + * Initialize magnetic button effect (advanced) + * Button follows cursor slightly when hovered + */ +export function initializeMagneticButtons(): void { + const magneticButtons = document.querySelectorAll('.magnetic-button'); + + magneticButtons.forEach((button) => { + const element = button as HTMLElement; + + button.addEventListener('mousemove', (e) => { + const rect = element.getBoundingClientRect(); + const x = (e as MouseEvent).clientX - rect.left - rect.width / 2; + const y = (e as MouseEvent).clientY - rect.top - rect.height / 2; + + element.style.transform = `translate(${x * MAGNETIC_BUTTON.TRANSFORM_MULTIPLIER}px, ${y * MAGNETIC_BUTTON.TRANSFORM_MULTIPLIER}px) scale(${MAGNETIC_BUTTON.HOVER_SCALE})`; + }); + + button.addEventListener('mouseleave', () => { + element.style.transform = `translate(0, 0) scale(${MAGNETIC_BUTTON.DEFAULT_SCALE})`; + }); + }); +} + +/** + * Initialize entrance animations for sections on scroll + */ +export function initializeScrollAnimations(): void { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + // Optionally unobserve after animation + // observer.unobserve(entry.target); + } + }); + }, + { + threshold: OBSERVER_THRESHOLDS.LOW, + rootMargin: ROOT_MARGINS.DEFAULT, + }, + ); + + // Observe elements with animation classes + const animatedElements = document.querySelectorAll( + '.trust-badge, .problem-card, .solution-card, .layer-card, .contributor-avatar', + ); + animatedElements.forEach((element) => observer.observe(element)); +} + +/** + * Initialize step card animations when "How It Works" section scrolls into view + */ +export function initializeStepCardAnimations(): void { + const howItWorksSection = document.querySelector('#how-it-works'); + if (!howItWorksSection) return; + + // Mark all containers as will-animate on initialization + const containers = howItWorksSection.querySelectorAll('.step-cards-container'); + containers.forEach((container) => { + container.classList.add('will-animate'); + }); + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && !entry.target.classList.contains('step-cards-animated')) { + // Mark section as animated to prevent re-triggering + entry.target.classList.add('step-cards-animated'); + + // Animate visible step cards with stagger + const visibleContainer = entry.target.querySelector( + '.tab-content:not(.hidden)', + ) as HTMLElement; + + if (visibleContainer) { + const cards = visibleContainer.querySelectorAll('[data-step-index] .step-card'); + + cards.forEach((card, index) => { + // Trigger animation with stagger delay + setTimeout(() => { + card.classList.add('animate-in'); + + // Animate step number as well + const stepNumber = card.querySelector('.step-number'); + if (stepNumber) { + stepNumber.classList.add('animate-in'); + } + + // Remove will-animate and add animated class after last card completes + if (index === cards.length - 1) { + setTimeout(() => { + visibleContainer.classList.remove('will-animate'); + visibleContainer.classList.add('animated'); + }, 800); // Wait for animation transition to complete (increased from 600ms) + } + }, index * 200); // Increased from ANIMATION_DURATIONS.COUNTER_STAGGER_DELAY (100ms) to 200ms + }); + } + + // Unobserve after animation triggers + observer.unobserve(entry.target); + } + }); + }, + { + threshold: OBSERVER_THRESHOLDS.LOW, + rootMargin: ROOT_MARGINS.DEFAULT, + }, + ); + + observer.observe(howItWorksSection); +} + +/** + * Check if animations should be disabled for performance + */ +function shouldDisableAnimations(): boolean { + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || + (navigator.maxTouchPoints > 0 && window.innerWidth < 1024); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + return isSafari || isMobile || prefersReducedMotion; +} + +/** + * Initialize all micro-interactions + */ +export function initializeMicroInteractions(): void { + // Always enable essential interactions (copy buttons) + initializeCopyButtons(); + + // Check if we should disable expensive animations + if (shouldDisableAnimations()) { + console.log('[Performance] Disabling expensive micro-interactions for better performance'); + + // Show step cards immediately without animation + const stepCards = document.querySelectorAll('.step-card'); + stepCards.forEach((card) => { + card.classList.remove('will-animate'); + const stepNumber = card.querySelector('.step-number'); + if (stepNumber) { + stepNumber.classList.add('animate-in'); + } + }); + + return; + } + + // Enable all animations on high-performance browsers (Chrome Desktop) + initializeTabAnimations(); + initializeMagneticButtons(); + initializeScrollAnimations(); + initializeStepCardAnimations(); +} + +// Auto-initialize when DOM is ready +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', () => { + initializeMicroInteractions(); + }); +} diff --git a/src/styles/animations/entrances.css b/src/styles/animations/entrances.css new file mode 100644 index 0000000..3b43daa --- /dev/null +++ b/src/styles/animations/entrances.css @@ -0,0 +1,242 @@ +/** + * Entrance animations for landing page sections + * Applied via class names with stagger delays + */ + +/* === GPU LAYER PROMOTION === */ +/* Safari: Forces GPU layer creation. Chrome: Redundant but harmless. */ +/* This dramatically improves Safari animation performance without affecting Chrome */ + +.trust-badge, +.headline-line-1, +.headline-line-2, +.headline-line-3, +.hero-subheadline, +.hero-cta-group, +.hero-metrics, +.demo-image, +.editor-badge, +.problem-card, +.solution-card, +.step-card, +.layer-card, +.contributor-avatar, +.path-card { + will-change: transform, opacity; + backface-visibility: hidden; + transform: translateZ(0); /* Force GPU layer */ +} + +/* === HERO SECTION === */ + +.trust-badge { + animation: slideDownFade 0.6s ease-out 0.2s backwards; +} + +.headline-line-1 { + animation: fadeInUp 0.6s ease-out 0.3s backwards; +} + +.headline-line-2 { + animation: fadeInUp 0.6s ease-out 0.5s backwards; +} + +.headline-line-3 { + animation: fadeInUp 0.6s ease-out 0.7s backwards; +} + +.hero-subheadline { + animation: fadeInUp 0.6s ease-out 0.9s backwards; +} + +.hero-cta-group { + animation: fadeInUp 0.6s ease-out 1.1s backwards; +} + +.editor-badge { + visibility: hidden; + animation: fadeInUpVisible 0.5s ease-out forwards; +} + +.editor-badge:nth-child(1) { + animation-delay: 1.3s; +} + +.editor-badge:nth-child(2) { + animation-delay: 1.45s; +} + +.editor-badge:nth-child(3) { + animation-delay: 1.6s; +} + +.editor-badge:nth-child(4) { + animation-delay: 1.75s; +} + +.hero-metrics { + animation: fadeInUp 0.6s ease-out 1.5s backwards; +} + +.demo-image { + animation: scaleInFade 1s ease-out 1.8s backwards; +} + +/* === PROBLEM/SOLUTION SECTION === */ + +.problem-card { + animation: slideInLeft 0.6s ease-out backwards; +} + +.solution-card { + animation: slideInRight 0.6s ease-out 0.2s backwards; +} + +/* === HOW IT WORKS SECTION === */ + +.step-number { + opacity: 0; +} + +.step-number.animate-in { + animation: fadeInGradual 0.8s ease-out forwards; +} + +.step-card:nth-child(1) .step-number.animate-in { + animation-delay: 0.2s; +} + +.step-card:nth-child(2) .step-number.animate-in { + animation-delay: 0.5s; +} + +.step-card:nth-child(3) .step-number.animate-in { + animation-delay: 0.8s; +} + +/* === FEATURES SECTION === */ + +.features-column-left { + animation: fadeInLeft 0.8s ease-out backwards; +} + +.features-column-right { + animation: fadeInRight 0.8s ease-out 0.2s backwards; +} + +/* === TECH STACK SECTION === */ + +.layer-card { + opacity: 0; + animation: popIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; + contain: layout style paint; /* Isolate layout calculations to prevent parent reflow */ +} + +.layer-card:nth-child(1) { + animation-delay: 0.1s; +} + +.layer-card:nth-child(2) { + animation-delay: 0.2s; +} + +.layer-card:nth-child(3) { + animation-delay: 0.3s; +} + +.layer-card:nth-child(4) { + animation-delay: 0.4s; +} + +.layer-card:nth-child(5) { + animation-delay: 0.5s; +} + +.layer-card:nth-child(6) { + animation-delay: 0.6s; +} + +/* === COMMUNITY SECTION === */ + +.contributor-avatar { + opacity: 0; + animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; +} + +.contributor-avatar:nth-child(1) { + animation-delay: 0.1s; +} + +.contributor-avatar:nth-child(2) { + animation-delay: 0.15s; +} + +.contributor-avatar:nth-child(3) { + animation-delay: 0.2s; +} + +.contributor-avatar:nth-child(4) { + animation-delay: 0.25s; +} + +.contributor-avatar:nth-child(5) { + animation-delay: 0.3s; +} + +.contributor-avatar:nth-child(6) { + animation-delay: 0.35s; +} + +.contributor-avatar:nth-child(7) { + animation-delay: 0.4s; +} + +.contributor-avatar:nth-child(8) { + animation-delay: 0.45s; +} + +/* === PERFORMANCE: DISABLE ENTRANCE ANIMATIONS === */ +/* Safari, mobile, and reduced motion: Show content immediately without animations */ +/* This eliminates layout shifts and improves scroll FPS from 25fps to 60fps */ + +.is-safari *[class*='animate'], +.is-safari *[class*='headline'], +.is-safari *[class*='trust-badge'], +.is-safari *[class*='hero'], +.is-safari *[class*='editor-badge'], +.is-safari *[class*='demo-image'], +.is-safari *[class*='problem-card'], +.is-safari *[class*='solution-card'], +.is-safari *[class*='step'], +.is-safari *[class*='layer-card'], +.is-safari *[class*='contributor-avatar'], +.is-safari *[class*='features-column'], +.is-mobile *[class*='animate'], +.is-mobile *[class*='headline'], +.is-mobile *[class*='trust-badge'], +.is-mobile *[class*='hero'], +.is-mobile *[class*='editor-badge'], +.is-mobile *[class*='demo-image'], +.is-mobile *[class*='problem-card'], +.is-mobile *[class*='solution-card'], +.is-mobile *[class*='step'], +.is-mobile *[class*='layer-card'], +.is-mobile *[class*='contributor-avatar'], +.is-mobile *[class*='features-column'], +.prefers-reduced-motion *[class*='animate'], +.prefers-reduced-motion *[class*='headline'], +.prefers-reduced-motion *[class*='trust-badge'], +.prefers-reduced-motion *[class*='hero'], +.prefers-reduced-motion *[class*='editor-badge'], +.prefers-reduced-motion *[class*='demo-image'], +.prefers-reduced-motion *[class*='problem-card'], +.prefers-reduced-motion *[class*='solution-card'], +.prefers-reduced-motion *[class*='step'], +.prefers-reduced-motion *[class*='layer-card'], +.prefers-reduced-motion *[class*='contributor-avatar'], +.prefers-reduced-motion *[class*='features-column'] { + animation: none !important; + opacity: 1 !important; + visibility: visible !important; + transform: none !important; +} diff --git a/src/styles/animations/keyframes.css b/src/styles/animations/keyframes.css new file mode 100644 index 0000000..0ef58fc --- /dev/null +++ b/src/styles/animations/keyframes.css @@ -0,0 +1,213 @@ +/** + * Reusable keyframe animations for landing page + * All animations designed for 60fps performance using transform and opacity + */ + +/* === ENTRANCE ANIMATIONS === */ + +@keyframes slideDownFade { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUpVisible { + from { + visibility: hidden; + opacity: 0; + transform: translateY(20px); + } + 1% { + visibility: visible; + } + to { + visibility: visible; + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleInFade { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes fadeInGradual { + 0% { + opacity: 0; + transform: scale(0.8); + } + 50% { + opacity: 0.5; + transform: scale(0.9); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes popIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes bounceIn { + from { + opacity: 0; + transform: scale(0); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* === INTERACTION ANIMATIONS === */ + +@keyframes bounce { + 0%, + 100% { + transform: scale(1.2) translateY(0); + } + 50% { + transform: scale(1.2) translateY(-4px); + } +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); + } + 50% { + transform: scale(1.05); + box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); + } +} + +@keyframes successPulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } +} + +@keyframes pulse-ring { + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(1.5); + opacity: 0; + } +} + +/* === SHIMMER ANIMATIONS === */ + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@keyframes shimmerSlide { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} + +/* === FOCUS ANIMATIONS === */ + +@keyframes focusPulse { + 0%, + 100% { + outline-color: rgba(59, 130, 246, 0.8); + } + 50% { + outline-color: rgba(20, 184, 166, 0.8); + } +} diff --git a/src/styles/animations/micro-interactions.css b/src/styles/animations/micro-interactions.css new file mode 100644 index 0000000..c21e995 --- /dev/null +++ b/src/styles/animations/micro-interactions.css @@ -0,0 +1,323 @@ +/** + * Micro-interactions and hover effects + * Subtle animations for user feedback + */ + +/* === EDITOR BADGES === */ + +.editor-badge { + position: relative; + transition: all 0.2s ease; + contain: layout style paint; /* Isolate layout calculations to prevent parent reflow */ +} + +.editor-badge::after { + content: ''; + position: absolute; + inset: -8px; + background: radial-gradient(circle, rgba(20, 184, 166, 0.2) 0%, transparent 70%); + border-radius: inherit; + opacity: 0; + transition: opacity 0.2s ease; + z-index: -1; +} + +@media (hover: hover) and (pointer: fine) { + .editor-badge:hover { + transform: translateY(-4px) scale(1.05); + border-color: rgba(20, 184, 166, 0.5); + } + + .editor-badge:hover::after { + opacity: 1; + } +} + +/* === CARDS === */ + +.before-after-card { + position: relative; + transition: transform 0.3s ease; +} + +.before-after-card::after { + content: ''; + position: absolute; + inset: -12px; + background: radial-gradient(circle, rgba(0, 0, 0, 0.4) 0%, transparent 70%); + border-radius: inherit; + opacity: 0; + transition: opacity 0.3s ease; + z-index: -1; +} + +@media (hover: hover) and (pointer: fine) { + .before-after-card:hover { + transform: translateY(-8px); + } + + .before-after-card:hover::after { + opacity: 1; + } +} + +.solution-card { + position: relative; +} + +.solution-card::after { + content: ''; + position: absolute; + inset: -12px; + background: radial-gradient(circle, rgba(20, 184, 166, 0.15) 0%, transparent 70%); + border-radius: inherit; + opacity: 0; + transition: opacity 0.3s ease; + z-index: -1; +} + +@media (hover: hover) and (pointer: fine) { + .solution-card:hover { + border-color: rgba(20, 184, 166, 0.6); + } + + .solution-card:hover::after { + opacity: 1; + } +} + +/* === ICONS === */ + +.emoji-icon { + display: inline-block; + transition: transform 0.2s ease; +} + +/* .before-after-card:hover .emoji-icon { + transform: scale(1.2); + animation: bounce 0.6s ease; +} */ + +.feature-icon { + display: inline-block; + transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +/* === FEATURE ITEMS === */ + +.feature-item { + transition: all 0.3s ease; +} + +@media (hover: hover) and (pointer: fine) { + .feature-item:hover { + transform: translateX(8px); + } +} + +/* === PATH CARDS === */ + +.path-card { + position: relative; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + contain: layout style paint; /* Isolate layout calculations to prevent parent reflow */ +} + +/* Ensure all direct children are above the pseudo-elements */ +.path-card > * { + position: relative; + z-index: 1; +} + +.path-card::after { + content: ''; + position: absolute; + inset: -20px; + background: radial-gradient(circle, rgba(0, 0, 0, 0.4) 0%, transparent 60%); + border-radius: inherit; + opacity: 0; + transition: opacity 0.4s ease; + z-index: -1; +} + +.path-card::before { + content: ''; + position: absolute; + inset: -1px; + background: rgba(59, 130, 246, 0.3); + border-radius: inherit; + opacity: 0; + transition: opacity 0.4s ease; + z-index: -1; +} + +@media (hover: hover) and (pointer: fine) { + .path-card:hover { + transform: translateY(-12px) scale(1.02); + border-color: rgba(59, 130, 246, 0.5); + } + + .path-card:hover::after, + .path-card:hover::before { + opacity: 1; + } + + .path-card:hover .feature-checkmark { + transform: scale(1.2); + color: rgba(20, 184, 166, 1); + } +} + +.path-icon { + display: inline-block; + position: relative; + z-index: 1; /* Ensure icon stays above pseudo-elements */ +} + +.feature-checkmark { + display: inline-block; + transition: all 0.2s ease; +} + +/* === BUTTONS === */ + +.cta-button { + position: relative; + overflow: hidden; +} + +/* Pulse ring effect for CTA buttons */ +.cta-button::before { + content: ''; + position: absolute; + inset: 0; + border: 2px solid rgba(59, 130, 246, 0.5); + border-radius: inherit; + animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + pointer-events: none; +} + +.copy-button { + transition: all 0.2s ease; +} + +@media (hover: hover) and (pointer: fine) { + .copy-button:hover { + transform: scale(1.05); + background: rgba(20, 184, 166, 0.1); + } +} + +.copy-button:active { + transform: scale(0.95); +} + +.copy-button.copied { + background: rgba(34, 197, 94, 0.2); + animation: successPulse 0.6s ease-out; +} + +/* === STEP NUMBERS === */ + +/* Step numbers no longer pulse - they appear once and stay static */ + +/* === STEP CARDS STAGGER === */ + +/* Cards are visible by default (for graceful degradation) */ +.step-card { + opacity: 1; + transform: translateY(0); + margin-top: 1rem; /* Reserve space for absolute positioned step number */ + contain: layout; /* Isolate layout calculations */ +} + +/* When container is marked as will-animate, hide cards and prepare for entrance */ +.step-cards-container.will-animate .step-card { + opacity: 0; + transform: translateY(30px); +} + +/* Animate in state with smooth transition */ +.step-card.animate-in { + opacity: 1 !important; + transform: translateY(0) !important; + transition: + opacity 0.6s ease-out, + transform 0.6s cubic-bezier(0.16, 1, 0.3, 1); +} + +/* === COMMUNITY AVATARS === */ + +.contributor-avatar { + position: relative; + transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +.contributor-avatar::after { + content: ''; + position: absolute; + inset: -8px; + background: radial-gradient(circle, rgba(20, 184, 166, 0.3) 0%, transparent 70%); + border-radius: inherit; + opacity: 0; + transition: opacity 0.3s ease; + z-index: -1; +} + +@media (hover: hover) and (pointer: fine) { + .contributor-avatar:hover { + transform: scale(1.15) rotate(5deg); + border-color: rgba(20, 184, 166, 0.8); + z-index: 10; + } + + .contributor-avatar:hover::after { + opacity: 1; + } +} + +/* === FINAL CTA SHIMMER === */ + +.final-cta-button { + position: relative; + overflow: hidden; +} + +.final-cta-button::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + animation: shimmerSlide 3s ease-in-out infinite; + pointer-events: none; +} + +/* === SHIMMER TEXT === */ + +.total-count { + position: relative; + display: inline-block; + background: linear-gradient( + 90deg, + rgba(59, 130, 246, 1) 0%, + rgba(20, 184, 166, 1) 50%, + rgba(59, 130, 246, 1) 100% + ); + background-size: 200% 100%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer 3s ease-in-out infinite; +} + +/* === FOCUS STATES === */ + +a:focus-visible, +button:focus-visible { + outline: 2px solid rgba(59, 130, 246, 0.8); + outline-offset: 4px; + animation: focusPulse 1s ease-in-out infinite; +} diff --git a/src/styles/animations/reduced-motion.css b/src/styles/animations/reduced-motion.css new file mode 100644 index 0000000..72159d8 --- /dev/null +++ b/src/styles/animations/reduced-motion.css @@ -0,0 +1,49 @@ +/** + * Accessibility: Reduced motion preferences + * Respects user's prefers-reduced-motion setting + */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable gradient orbs entirely - too expensive for reduced motion */ + .gradient-orb { + display: none !important; + } + + /* Disable shimmer effects */ + .total-count, + .final-cta-button::after { + animation: none !important; + } + + /* Disable pulse rings */ + .cta-button::before { + animation: none !important; + } + + /* Keep focus indicators but remove animation */ + a:focus-visible, + button:focus-visible { + animation: none !important; + } + + /* Simplify transforms to immediate states */ + .editor-badge:hover, + .before-after-card:hover, + .path-card:hover, + .contributor-avatar:hover { + transform: none !important; + } + + .feature-item:hover .feature-icon { + transform: none !important; + } +} diff --git a/src/styles/global.css b/src/styles/global.css index 58e56f9..2fbe22c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,6 +1,12 @@ @import 'tailwindcss'; @plugin "@tailwindcss/typography"; +/* Landing page animations */ +@import './animations/keyframes.css'; +@import './animations/entrances.css'; +@import './animations/micro-interactions.css'; +@import './animations/reduced-motion.css'; + @theme { --font-mono: 'Noto Sans Mono', monospace; } @@ -50,6 +56,17 @@ } } + @keyframes scrollFadeIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .animate-accordion-down { animation: accordion-down 0.2s ease-out; } @@ -59,16 +76,106 @@ } .animate-fade-in { - animation: fadeIn 0.2s ease-out forwards; + animation: scrollFadeIn 0.8s ease-out forwards; } .animate-fade-out { animation: fadeOut 0.2s ease-out forwards; } + .animate-on-scroll { + opacity: 0; + transform: translateY(30px); + transition: + opacity 0.8s ease-out, + transform 0.8s ease-out; + } + + .animate-on-scroll.animate-fade-in { + opacity: 1; + transform: translateY(0); + } + button { cursor: pointer; } + + /* Landing page background patterns and gradients */ + .bg-grid-pattern { + background-image: + linear-gradient(to right, rgba(75, 85, 99, 0.1) 1px, transparent 1px), + linear-gradient(to bottom, rgba(75, 85, 99, 0.1) 1px, transparent 1px); + background-size: 40px 40px; + } + + .bg-dots-pattern { + background-image: radial-gradient(rgba(75, 85, 99, 0.3) 1px, transparent 1px); + background-size: 20px 20px; + } + + .gradient-orb { + position: absolute; + border-radius: 50%; + filter: blur(80px); /* Full quality for capable browsers (Chrome Desktop) */ + opacity: 0.3; + pointer-events: none; + animation: float 20s ease-in-out infinite; + will-change: transform; + } + + /* Performance: Disable expensive gradient orbs on Safari, mobile, and reduced motion */ + /* Gradient orbs with blur + animation are too expensive for these devices/preferences */ + .is-safari .gradient-orb, + .is-mobile .gradient-orb, + .prefers-reduced-motion .gradient-orb { + display: none; /* Hide entirely for smooth 60fps scrolling */ + } + + .gradient-orb-blue { + background: radial-gradient(circle, rgba(59, 130, 246, 0.5) 0%, rgba(59, 130, 246, 0) 70%); + } + + .gradient-orb-teal { + background: radial-gradient(circle, rgba(20, 184, 166, 0.5) 0%, rgba(20, 184, 166, 0) 70%); + } + + .gradient-orb-purple { + background: radial-gradient(circle, rgba(168, 85, 247, 0.5) 0%, rgba(168, 85, 247, 0) 70%); + } + + @keyframes float { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(30px, -30px) scale(1.1); + } + 66% { + transform: translate(-20px, 20px) scale(0.9); + } + } + + .gradient-mesh { + background: + radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.08) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(20, 184, 166, 0.08) 0px, transparent 50%), + radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.08) 0px, transparent 50%), + radial-gradient(at 0% 100%, rgba(59, 130, 246, 0.08) 0px, transparent 50%); + filter: blur(40px); + } + + /* Performance: Simplify background patterns on mobile */ + .is-mobile .bg-grid-pattern, + .is-mobile .bg-dots-pattern { + background-image: none; + background-color: transparent; + } + + .is-mobile .gradient-mesh { + filter: none; + opacity: 0.3; + } } /* Dark mode scrollbar styles to match the dark theme UI */ diff --git a/src/utils/landingData.ts b/src/utils/landingData.ts new file mode 100644 index 0000000..8f49ad0 --- /dev/null +++ b/src/utils/landingData.ts @@ -0,0 +1,155 @@ +/** + * Landing page data utilities + * Helpers to extract and format data for the landing page + */ + +import { Layer, getLibrariesCountByLayer } from '../data/dictionaries'; + +// Contributor interface based on all-contributors format +export interface Contributor { + name: string; + login: string; + avatar_url: string; + profile: string; + contributions: string[]; +} + +// Tech stack layer statistics +export interface LayerStats { + name: string; + icon: string; + count: number; + layer: Layer; +} + +/** + * Get library counts by layer with formatted display data + */ +export const getLayerStatistics = (): LayerStats[] => { + return [ + { + name: 'Coding Practices', + icon: 'πŸ’‘', + count: getLibrariesCountByLayer(Layer.CODING_PRACTICES), + layer: Layer.CODING_PRACTICES, + }, + { + name: 'Frontend', + icon: '🎨', + count: getLibrariesCountByLayer(Layer.FRONTEND), + layer: Layer.FRONTEND, + }, + { + name: 'Backend', + icon: 'βš™οΈ', + count: getLibrariesCountByLayer(Layer.BACKEND), + layer: Layer.BACKEND, + }, + { + name: 'Database', + icon: 'πŸ—„οΈ', + count: getLibrariesCountByLayer(Layer.DATABASE), + layer: Layer.DATABASE, + }, + { + name: 'DevOps', + icon: 'πŸš€', + count: getLibrariesCountByLayer(Layer.DEVOPS), + layer: Layer.DEVOPS, + }, + { + name: 'Testing', + icon: 'βœ…', + count: getLibrariesCountByLayer(Layer.TESTING), + layer: Layer.TESTING, + }, + ]; +}; + +/** + * Get total library count across all layers + */ +export const getTotalLibraryCount = (): number => { + return Object.values(Layer).reduce((total, layer) => { + return total + getLibrariesCountByLayer(layer); + }, 0); +}; + +/** + * Get contributor data from README.md + * This is a static list extracted from the README.md all-contributors section + */ +export const getContributors = (): Contributor[] => { + return [ + { + name: 'Damian', + login: 'damianidczak', + avatar_url: 'https://avatars.githubusercontent.com/u/21343496?v=4', + profile: 'https://github.com/damianidczak', + contributions: ['code'], + }, + { + name: 'pawel-twardziak', + login: 'pawel-twardziak', + avatar_url: 'https://avatars.githubusercontent.com/u/180847852?v=4', + profile: 'https://github.com/pawel-twardziak', + contributions: ['code'], + }, + { + name: 'Michal Dudziak', + login: 'dudziakm', + avatar_url: 'https://avatars.githubusercontent.com/u/10773170?v=4', + profile: 'https://github.com/dudziakm', + contributions: ['maintenance'], + }, + { + name: 'Artur Laskowski', + login: 'arturlaskowski', + avatar_url: 'https://avatars.githubusercontent.com/u/92392161?v=4', + profile: 'https://www.linkedin.com/in/artur-laskowski94', + contributions: ['code'], + }, + { + name: 'Michaelzag', + login: 'Michaelzag', + avatar_url: 'https://avatars.githubusercontent.com/u/4809030?v=4', + profile: 'https://github.com/Michaelzag', + contributions: ['code'], + }, + { + name: 'Piotr Porzuczek', + login: 'PeterPorzuczek', + avatar_url: 'https://avatars.githubusercontent.com/u/24259570?v=4', + profile: 'https://github.com/PeterPorzuczek', + contributions: ['code'], + }, + { + name: 'MichaΕ‚ Michalczuk', + login: 'michalczukm', + avatar_url: 'https://avatars.githubusercontent.com/u/6861120?v=4', + profile: 'https://michalczukm.xyz', + contributions: ['code'], + }, + { + name: 'PaweΕ‚ Gnat', + login: 'Pawel-Gnat', + avatar_url: 'https://avatars.githubusercontent.com/u/104066590?v=4', + profile: 'https://www.pawelgnat.com/', + contributions: ['code'], + }, + { + name: 'Kacper KΕ‚osowski', + login: 'kacperklosowski', + avatar_url: 'https://avatars.githubusercontent.com/u/77013552?v=4', + profile: 'https://github.com/kacperklosowski', + contributions: ['code'], + }, + ]; +}; + +/** + * Get contributor count + */ +export const getContributorCount = (): number => { + return getContributors().length; +};