diff --git a/resources/js/pages/marketing/Blog.tsx b/resources/js/pages/marketing/Blog.tsx index 3c1b104..ebdde0a 100644 --- a/resources/js/pages/marketing/Blog.tsx +++ b/resources/js/pages/marketing/Blog.tsx @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { motion, useReducedMotion } from 'framer-motion'; interface PostSummary { id: number; @@ -56,12 +57,26 @@ const Blog: React.FC = ({ posts }) => { ); const { t, i18n } = useTranslation('marketing'); const locale = i18n.language || 'de'; + const shouldReduceMotion = useReducedMotion(); const articles = posts?.data ?? []; const isLandingLayout = posts.current_page === 1; const featuredPost = isLandingLayout ? articles[0] : null; const gridPosts = isLandingLayout ? articles.slice(1, 4) : []; const listPosts = isLandingLayout ? [] : articles; const dateLocale = locale === 'en' ? 'en-US' : 'de-DE'; + const viewportOnce = { once: true, amount: 0.25 }; + const revealUp = { + hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 18 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] }, + }, + }; + const stagger = { + hidden: {}, + visible: { transition: { staggerChildren: 0.12 } }, + }; const buildArticleHref = React.useCallback( (slug?: string | null) => { @@ -194,8 +209,20 @@ const Blog: React.FC = ({ posts }) => { return (
-
-
+ + {t('blog.posts_title')} @@ -213,9 +240,15 @@ const Blog: React.FC = ({ posts }) => { {t('blog.read_more')} -
-
-
+ + +
{featuredPost.featured_image && ( = ({ posts }) => {
)}
-
-
+ + {gridPosts.length > 0 && ( -
+ {gridPosts.map((post) => ( - - {post.featured_image && ( -
- {post.title} -
- )} - -
-

- {post.title || 'Untitled'} -

- -
-
- {renderPostMeta(post)} - - - + + + {post.featured_image && ( +
+ {post.title} +
+ )} + +
+

+ {post.title || 'Untitled'} +

+ +
+
+ {renderPostMeta(post)} + + + + ))} -
+
)}
); }; const renderListView = () => ( -
+ {listPosts.map((post) => ( - -
- {post.featured_image && ( - {post.title} - )} - -
-

- {post.title || 'Untitled'} -

- + +
+ {post.featured_image && ( + {post.title} - {renderPostMeta(post)} - -
- -
- + )} + +
+

+ {post.title || 'Untitled'} +

+ + {renderPostMeta(post)} + +
+
+
+
+
))} {listPosts.length === 0 && ( - {t('blog.empty')} + + {t('blog.empty')} + )} -
+
); return ( @@ -312,29 +363,46 @@ const Blog: React.FC = ({ posts }) => {
-
- - Fotospiel Blog - -

{t('blog.hero_title')}

-

{t('blog.hero_description')}

-
+ + + + Fotospiel Blog + + + + {t('blog.hero_title')} + + + {t('blog.hero_description')} + + -
-
+ +
-
+
-
+

{t('blog.posts_title')}

-
+ {isLandingLayout ? renderLandingGrid() : renderListView()} diff --git a/resources/js/pages/marketing/BlogShow.tsx b/resources/js/pages/marketing/BlogShow.tsx index bb6b65e..1d7d5ff 100644 --- a/resources/js/pages/marketing/BlogShow.tsx +++ b/resources/js/pages/marketing/BlogShow.tsx @@ -7,6 +7,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; +import { motion, useReducedMotion } from 'framer-motion'; interface AdjacentPost { slug: string; @@ -58,6 +59,20 @@ const BlogShow: React.FC = ({ post }) => { const [copied, setCopied] = React.useState(false); const locale = i18n.language || 'de'; const dateLocale = locale === 'en' ? 'en-US' : 'de-DE'; + const shouldReduceMotion = useReducedMotion(); + const viewportOnce = { once: true, amount: 0.25 }; + const revealUp = { + hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 18 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] }, + }, + }; + const stagger = { + hidden: {}, + visible: { transition: { staggerChildren: 0.12 } }, + }; const formattedDate = React.useMemo(() => { try { @@ -121,9 +136,9 @@ const BlogShow: React.FC = ({ post }) => { -
-
-
+
+ +
+ - - -
-
- - Fotospiel Stories - -

{post.title}

-
- {t('by_author')} {post.author?.name || t('team')} - - {t('published_on')} {formattedDate} -
- -
-
- {post.featured_image ? ( -
- {post.title} -
- ) : ( -
+ + + +
+
+ Fotospiel Stories + +

{post.title}

+
+ {t('by_author')} {post.author?.name || t('team')} + + {t('published_on')} {formattedDate}
- )} + +
+
+ {post.featured_image ? ( +
+ {post.title} +
+ ) : ( +
+ Fotospiel Stories +
+ )} +
-
- - -
+ + + +
-
-
-
+
+ +
-
+ - -
+ {canUseNativeShare && ( + + )} +
+ +
+ {shareLinks.map((link) => ( + + ))} +
+ + + + +
{(post.previous_post || post.next_post) && ( -
-
+
+
{post.previous_post && ( - - -

- {t('previous_post')} -

-

{post.previous_post.title}

- - -
-
+ + + +

+ {t('previous_post')} +

+

{post.previous_post.title}

+ + +
+
+
)} {post.next_post && ( - - -

- {t('next_post')} -

-

{post.next_post.title}

- - -
-
+ + + +

+ {t('next_post')} +

+

{post.next_post.title}

+ + +
+
+
)}
-
+
)} diff --git a/resources/js/pages/marketing/Demo.tsx b/resources/js/pages/marketing/Demo.tsx index 6589c5c..d333b49 100644 --- a/resources/js/pages/marketing/Demo.tsx +++ b/resources/js/pages/marketing/Demo.tsx @@ -10,6 +10,7 @@ import { Head, Link } from '@inertiajs/react'; import { CheckCircle2, Sparkles } from 'lucide-react'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { motion, useReducedMotion } from 'framer-motion'; type DemoFeature = { title: string; description: string }; @@ -21,6 +22,7 @@ const DemoPage: React.FC = ({ demoToken }) => { const { t } = useTranslation('marketing'); const { localizedPath } = useLocalizedRoutes(); const locale = useLocale(); + const shouldReduceMotion = useReducedMotion(); const embedUrl = demoToken ? `/e/${demoToken}` : '/e/demo?demo=1'; const [isDemoOpen, setIsDemoOpen] = React.useState(false); const demoOpenLabel = t('labels.demoOpenOverlay', locale === 'en' ? 'Open demo overlay' : 'Demo im Overlay öffnen'); @@ -38,6 +40,25 @@ const DemoPage: React.FC = ({ demoToken }) => { const handleOpenDemo = (): void => { setIsDemoOpen(true); }; + const frameVariants = shouldReduceMotion + ? { rest: {}, hover: {} } + : { + rest: { y: 0, scale: 1, rotateX: 0, rotateY: 0 }, + hover: { + y: -6, + scale: 1.01, + rotateX: -2, + rotateY: 3, + transition: { type: 'spring', stiffness: 180, damping: 20 }, + }, + }; + const shineVariants = shouldReduceMotion + ? { rest: {}, hover: {} } + : { + rest: { x: '-120%', opacity: 0 }, + hover: { x: '120%', opacity: 0.8, transition: { duration: 0.8, ease: 'easeOut' } }, + }; + const featureHover = shouldReduceMotion ? {} : { y: -4, scale: 1.01 }; return ( @@ -74,28 +95,46 @@ const DemoPage: React.FC = ({ demoToken }) => {
{embedUrl ? ( <> -
-
-
- {isDemoOpen ? ( -
-

Live Demo

-

Demo läuft im Overlay

-

Schließe das Overlay, um hier weiterzumachen.

-
- ) : ( -