diff --git a/frontend/app/[locale]/page.tsx b/frontend/app/[locale]/page.tsx index c7e1ee1c..072e55c3 100644 --- a/frontend/app/[locale]/page.tsx +++ b/frontend/app/[locale]/page.tsx @@ -1,5 +1,5 @@ import { getTranslations } from 'next-intl/server'; -import HeroSection from '@/components/shared/HeroSection'; +import HeroSection from '@/components/home/HeroSection'; export async function generateMetadata({ params, diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a56f36df..a5a92c93 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -30,8 +30,7 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); - --animate-float-soft: float-soft 8s ease-in-out infinite; - --animate-pulse-soft: pulse-soft 2.8s ease-in-out infinite; + --animate-wave-clip: wave-clip 5s ease-in-out infinite; } @theme { @@ -154,51 +153,183 @@ user-select: none; /* Standard */ } -@keyframes float-soft { +@keyframes progress { + from { + width: 100%; + } + to { + width: 0%; + } +} + +@keyframes shrink { + from { + transform: scaleX(1); + } + to { + transform: scaleX(0); + } +} + +@keyframes wave-clip { 0%, 100% { - transform: translateY(8px) scale(1); + clip-path: polygon( + 0% 50%, + 15% 48%, + 32% 52%, + 54% 60%, + 70% 62%, + 84% 60%, + 100% 55%, + 100% 100%, + 0% 100% + ); } 50% { - transform: translateY(-16px) scale(1.04); + clip-path: polygon( + 0% 65%, + 16% 70%, + 34% 72%, + 51% 68%, + 67% 58%, + 84% 52%, + 100% 48%, + 100% 100%, + 0% 100% + ); } } -@keyframes pulse-soft { - 0%, +@keyframes slide-up-gradient { + 0% { + clip-path: inset(100% 0 0 0); + } 100% { - opacity: 0.2; - transform: scale(1); + clip-path: inset(0 0 0 0); + } +} + +@keyframes wave-slide-up { + 0% { + clip-path: polygon( + 0% 100%, + 10% 100%, + 20% 100%, + 30% 100%, + 40% 100%, + 50% 100%, + 60% 100%, + 70% 100%, + 80% 100%, + 90% 100%, + 100% 100%, + 100% 100%, + 0% 100% + ); } 50% { - opacity: 0.4; - transform: scale(1.08); + clip-path: polygon( + 0% 60%, + 10% 50%, + 20% 45%, + 30% 48%, + 40% 55%, + 50% 50%, + 60% 45%, + 70% 48%, + 80% 52%, + 90% 50%, + 100% 55%, + 100% 100%, + 0% 100% + ); + } + 100% { + clip-path: polygon( + 0% 0%, + 10% 0%, + 20% 0%, + 30% 0%, + 40% 0%, + 50% 0%, + 60% 0%, + 70% 0%, + 80% 0%, + 90% 0%, + 100% 0%, + 100% 100%, + 0% 100% + ); } } -@keyframes progress { - from { - width: 100%; +@keyframes text-fade-in { + 0% { + opacity: 0; + transform: translateY(4px); } - to { - width: 0%; + 100% { + opacity: 1; + transform: translateY(0); } } -@keyframes shimmer { + +@keyframes card-breathe { 0% { - background-position: 200% 0; + transform: translate(0, 0) scale(1) rotate(var(--card-rotate, 0deg)); + } + 50% { + transform: translate(var(--card-x, 0), var(--card-y, 0)) scale(1.05) + rotate(calc(var(--card-rotate, 0deg) + var(--card-rotate-offset, 0deg))); } 100% { - background-position: -200% 0; + transform: translate(0, 0) scale(1) rotate(var(--card-rotate, 0deg)); } } -@keyframes shrink { - from { - transform: scaleX(1); +@layer utilities { + .wave-text-gradient { + animation: wave-clip 5s ease-in-out infinite; } - to { - transform: scaleX(0); + + .animate-slide-up-gradient { + animation: slide-up-gradient 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .animate-wave-slide-up { + animation: wave-slide-up 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .animate-text-fade-in { + animation: text-fade-in 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + + .animate-card-breathe { + animation: card-breathe 6s ease-in-out infinite; + will-change: transform; + } + + .left-4 .animate-card-breathe, + .md\:left-6 .animate-card-breathe, + .lg\:left-8 .animate-card-breathe, + .xl\:left-12 .animate-card-breathe { + --card-x: -30px; + --card-y: -25px; + --card-rotate: -10deg; + --card-rotate-offset: -3deg; + animation-delay: 0s; + } + + .right-4 .animate-card-breathe, + .md\:right-6 .animate-card-breathe, + .lg\:right-8 .animate-card-breathe, + .xl\:right-12 .animate-card-breathe { + --card-x: 30px; + --card-y: 25px; + --card-rotate: 8deg; + --card-rotate-offset: 3deg; + animation-delay: 0.8s; } } @@ -218,13 +349,11 @@ --color-ring: var(--foreground); --card: var(--background); - /* CTA button: light theme (section is black) -> pink bg + white text */ + /* CTA button: light theme (section is black) -> pink bg + white text */ --shop-cta-bg: #ff2d55; --shop-cta-fg: oklch(0.985 0 0); - } - .dark .shop-scope { /* dark: shop accent = magenta */ --accent: var(--accent-primary); @@ -241,10 +370,9 @@ /* keep borders closer to previous shop look, derived (no hex) */ --border: color-mix(in oklab, var(--foreground) 18%, var(--background)); --input: color-mix(in oklab, var(--foreground) 18%, var(--background)); - /* CTA button: dark theme (section becomes white) -> black bg + white text */ + /* CTA button: dark theme (section becomes white) -> black bg + white text */ --shop-cta-bg: var(--background); --shop-cta-fg: var(--foreground); - } /* about-community */ diff --git a/frontend/components/header/MainSwitcher.tsx b/frontend/components/header/MainSwitcher.tsx index 52dd41d5..63a313e0 100644 --- a/frontend/components/header/MainSwitcher.tsx +++ b/frontend/components/header/MainSwitcher.tsx @@ -61,11 +61,7 @@ export function MainSwitcher({ } return ( -
+
{children}
); diff --git a/frontend/components/home/CodeCard.tsx b/frontend/components/home/CodeCard.tsx new file mode 100644 index 00000000..6f3b1157 --- /dev/null +++ b/frontend/components/home/CodeCard.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react'; + +interface CodeCardProps { + fileName: string; + snippet: ReactNode; + className?: string; +} + +export function CodeCard({ fileName, snippet, className }: CodeCardProps) { + return ( + + ); +} diff --git a/frontend/components/home/HeroBackground.tsx b/frontend/components/home/HeroBackground.tsx new file mode 100644 index 00000000..56caf61b --- /dev/null +++ b/frontend/components/home/HeroBackground.tsx @@ -0,0 +1,19 @@ +export function HeroBackground() { + return ( + <> +
+
+
+
+
+ +
+ + + + + +
+ + ); +} diff --git a/frontend/components/home/HeroCodeCards.tsx b/frontend/components/home/HeroCodeCards.tsx new file mode 100644 index 00000000..7c4fd1b6 --- /dev/null +++ b/frontend/components/home/HeroCodeCards.tsx @@ -0,0 +1,56 @@ +import { CodeCard } from './CodeCard'; + +export function HeroCodeCards() { + return ( + <> + + type Arr1 = [ + + 'a' + + ,{' '} + + 'b' + + ,{' '} + + 'c' + + ]{'\n'} + type Arr2 = [ + 3,{' '} + 2,{' '} + 1] + + } + /> + + + function sum( + + a + ,{' '} + b){' '} + {'{'} + {'\n'} + {' '} + return{' '} + a{' '} + +{' '} + b; + {'\n'} + {'}'} + + } + /> + + ); +} diff --git a/frontend/components/home/HeroSection.tsx b/frontend/components/home/HeroSection.tsx new file mode 100644 index 00000000..78946cce --- /dev/null +++ b/frontend/components/home/HeroSection.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { HeroBackground } from './HeroBackground'; +import { HeroCodeCards } from './HeroCodeCards'; +import { InteractiveCTAButton } from './InteractiveCTAButton'; + +export default function HeroSection() { + const t = useTranslations('homepage'); + + return ( +
+ + +
+ + +

+ {t('subtitle')} +

+ +
+

+ + DevLovers + + + +

+
+ +

+ {t('description')} +

+ +
+ +
+
+
+ ); +} diff --git a/frontend/components/home/InteractiveCTAButton.tsx b/frontend/components/home/InteractiveCTAButton.tsx new file mode 100644 index 00000000..f6c84014 --- /dev/null +++ b/frontend/components/home/InteractiveCTAButton.tsx @@ -0,0 +1,76 @@ +'use client'; + +import React from 'react'; +import { useTranslations } from 'next-intl'; +import { Link } from '@/i18n/routing'; +import { createCTAVariants } from '@/lib/home/cta-variants'; + +export function InteractiveCTAButton() { + const t = useTranslations('homepage'); + + const [index, setIndex] = React.useState(0); + const [isHovered, setIsHovered] = React.useState(false); + + const variants = createCTAVariants(t('cta')); + const current = variants[index]; + const nextIndex = (index + 1) % variants.length; + const next = variants[nextIndex]; + + const handleEnter = () => { + if (!window.matchMedia('(hover: hover)').matches) return; + if (isHovered) return; + setIsHovered(true); + }; + + const handleLeave = () => { + if (!window.matchMedia('(hover: hover)').matches) return; + if (!isHovered) return; + setIndex(nextIndex); + setIsHovered(false); + }; + + return ( + + + + {isHovered && ( + + )} + + + + + + {isHovered ? next.text : current.text} + + + + ); +} diff --git a/frontend/components/shared/Footer.tsx b/frontend/components/shared/Footer.tsx index 5c802d6e..a355efa4 100644 --- a/frontend/components/shared/Footer.tsx +++ b/frontend/components/shared/Footer.tsx @@ -20,27 +20,7 @@ export default function Footer() { const t = useTranslations('footer'); return ( -