feat: add page transition animations and loading indicators
- Add nextjs-toploader for instant top progress bar on navigation - Add next-view-transitions for View Transitions API page transitions - Enhance template.tsx page enter animation (0.45s, scale effect) - Replace next/link with next-view-transitions Link for smooth transitions - Add prefers-reduced-motion support for accessibility Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { ThemeProvider } from 'next-themes';
|
|||||||
import { Playfair_Display, LXGW_WenKai_TC } from 'next/font/google';
|
import { Playfair_Display, LXGW_WenKai_TC } from 'next/font/google';
|
||||||
import { JsonLd } from '@/components/json-ld';
|
import { JsonLd } from '@/components/json-ld';
|
||||||
import { WebVitals } from '@/components/web-vitals';
|
import { WebVitals } from '@/components/web-vitals';
|
||||||
|
import { ViewTransitions } from 'next-view-transitions';
|
||||||
|
import NextTopLoader from 'nextjs-toploader';
|
||||||
|
|
||||||
const playfair = Playfair_Display({
|
const playfair = Playfair_Display({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -98,6 +100,7 @@ export default function RootLayout({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ViewTransitions>
|
||||||
<html lang={siteConfig.defaultLocale} suppressHydrationWarning className={`${playfair.variable} ${lxgwWenKai.variable}`}>
|
<html lang={siteConfig.defaultLocale} suppressHydrationWarning className={`${playfair.variable} ${lxgwWenKai.variable}`}>
|
||||||
<head>
|
<head>
|
||||||
{/* Preconnect to Google Fonts for faster font loading */}
|
{/* Preconnect to Google Fonts for faster font loading */}
|
||||||
@@ -105,6 +108,13 @@ export default function RootLayout({
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<NextTopLoader
|
||||||
|
color={theme.accent}
|
||||||
|
height={3}
|
||||||
|
showSpinner={false}
|
||||||
|
speed={200}
|
||||||
|
shadow={`0 0 10px ${theme.accent}, 0 0 5px ${theme.accent}`}
|
||||||
|
/>
|
||||||
<JsonLd data={websiteSchema} />
|
<JsonLd data={websiteSchema} />
|
||||||
<JsonLd data={organizationSchema} />
|
<JsonLd data={organizationSchema} />
|
||||||
<style
|
<style
|
||||||
@@ -126,5 +136,6 @@ export default function RootLayout({
|
|||||||
<WebVitals />
|
<WebVitals />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
</ViewTransitions>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import { getAllPostsSorted } from '@/lib/posts';
|
import { getAllPostsSorted } from '@/lib/posts';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { PostListItem } from '@/components/post-list-item';
|
import { PostListItem } from '@/components/post-list-item';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { FiTag, FiTrendingUp } from 'react-icons/fi';
|
import { FiTag, FiTrendingUp } from 'react-icons/fi';
|
||||||
import { getAllTagsWithCount } from '@/lib/posts';
|
import { getAllTagsWithCount } from '@/lib/posts';
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
export default function Template({ children }: { children: React.ReactNode }) {
|
export default function Template({ children }: { children: React.ReactNode }) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
setPrefersReducedMotion(mq.matches);
|
||||||
|
const handler = () => setPrefersReducedMotion(mq.matches);
|
||||||
|
mq.addEventListener('change', handler);
|
||||||
|
return () => mq.removeEventListener('change', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
if (prefersReducedMotion) {
|
||||||
|
container.style.animation = 'none';
|
||||||
|
container.style.opacity = '1';
|
||||||
|
container.style.transform = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger animation on mount
|
// Trigger animation on mount
|
||||||
container.style.animation = 'none';
|
container.style.animation = 'none';
|
||||||
// Force reflow
|
|
||||||
void container.offsetHeight;
|
void container.offsetHeight;
|
||||||
container.style.animation = 'pageEnter 0.3s cubic-bezier(0.32, 0.72, 0, 1) forwards';
|
container.style.animation = 'pageEnter 0.45s cubic-bezier(0.32, 0.72, 0, 1) forwards';
|
||||||
}, [children]);
|
}, [children, prefersReducedMotion]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="page-transition">
|
<div ref={containerRef} className="page-transition">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
FiChevronDown,
|
FiChevronDown,
|
||||||
FiChevronRight
|
FiChevronRight
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
export type IconKey =
|
export type IconKey =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import type { Post } from 'contentlayer2/generated';
|
import type { Post } from 'contentlayer2/generated';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Post } from 'contentlayer2/generated';
|
import { Post } from 'contentlayer2/generated';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import { Post } from 'contentlayer2/generated';
|
import { Post } from 'contentlayer2/generated';
|
||||||
import { FiArrowLeft, FiArrowRight } from 'react-icons/fi';
|
import { FiArrowLeft, FiArrowRight } from 'react-icons/fi';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import { FiExternalLink } from 'react-icons/fi';
|
import { FiExternalLink } from 'react-icons/fi';
|
||||||
import type { RepoSummary } from '@/lib/github';
|
import type { RepoSummary } from '@/lib/github';
|
||||||
import { getLanguageColor } from '@/lib/github-lang-colors';
|
import { getLanguageColor } from '@/lib/github-lang-colors';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FaGithub, FaMastodon, FaLinkedin } from 'react-icons/fa';
|
import { FaGithub, FaMastodon, FaLinkedin } from 'react-icons/fa';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import { Link } from 'next-view-transitions';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { ThemeToggle } from './theme-toggle';
|
import { ThemeToggle } from './theme-toggle';
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -18,6 +18,8 @@
|
|||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"next-contentlayer2": "^0.5.8",
|
"next-contentlayer2": "^0.5.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"next-view-transitions": "^0.3.5",
|
||||||
|
"nextjs-toploader": "^3.9.17",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
@@ -7265,7 +7267,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -7723,7 +7724,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
@@ -9125,6 +9125,17 @@
|
|||||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-view-transitions": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-view-transitions/-/next-view-transitions-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-yP8OPNydLmKpmE94hLurLGEzPsUy1uyl9iSv8oxaC2JwhSXTD86SVwk1NMMQT7Ado4kMENDJ7fNBIXHy3GU/Lg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": ">=14.0.0",
|
||||||
|
"react": ">=18.2.0 || ^19.0.0",
|
||||||
|
"react-dom": ">=18.2.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@@ -9153,6 +9164,24 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nextjs-toploader": {
|
||||||
|
"version": "3.9.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz",
|
||||||
|
"integrity": "sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://buymeacoffee.com/thesgj"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": ">= 6.0.0",
|
||||||
|
"react": ">= 16.0.0",
|
||||||
|
"react-dom": ">= 16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/no-case": {
|
"node_modules/no-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||||
@@ -9179,11 +9208,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nprogress": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -9613,7 +9647,6 @@
|
|||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
@@ -9719,7 +9752,6 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"next-contentlayer2": "^0.5.8",
|
"next-contentlayer2": "^0.5.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"next-view-transitions": "^0.3.5",
|
||||||
|
"nextjs-toploader": "^3.9.17",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
|||||||
@@ -122,11 +122,11 @@ body {
|
|||||||
@keyframes pageEnter {
|
@keyframes pageEnter {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(16px) scale(0.99);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +134,13 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.page-transition {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Scroll reveal animations - CSS only */
|
/* Scroll reveal animations - CSS only */
|
||||||
.scroll-reveal {
|
.scroll-reveal {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user