diff --git a/components/Layout/MainNavigator.tsx b/components/Layout/MainNavigator.tsx
index 8bff0fc..c5bc070 100644
--- a/components/Layout/MainNavigator.tsx
+++ b/components/Layout/MainNavigator.tsx
@@ -1,9 +1,8 @@
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
-import Link from 'next/link';
-
import { i18n, I18nContext, LanguageName } from '../../models/Translation';
+import { TransitionLink } from '../TransitionLink';
import { SymbolIcon } from '../Icon';
import { Button } from '../ui/button';
import {
@@ -28,9 +27,9 @@ export class MainNavigator extends ObservedComponent<{ menu: MenuLink[] }, typeo
renderLinks = () =>
this.props.menu.map(({ title, href, target }) => (
));
@@ -83,18 +82,18 @@ export class MainNavigator extends ObservedComponent<{ menu: MenuLink[] }, typeo
-
+
idea2app
-
+
{this.renderI18nSwitch()}
diff --git a/components/Layout/Section.tsx b/components/Layout/Section.tsx
index 163045a..4771ab0 100644
--- a/components/Layout/Section.tsx
+++ b/components/Layout/Section.tsx
@@ -1,8 +1,8 @@
import { observer } from 'mobx-react';
-import Link from 'next/link';
import { FC, PropsWithChildren, useContext } from 'react';
import { I18nContext } from '../../models/Translation';
+import { TransitionLink } from '../TransitionLink';
import { Button } from '../ui/button';
export type SectionProps = PropsWithChildren<
@@ -24,9 +24,9 @@ export const Section: FC
= observer(
{link && (
)}
diff --git a/components/Member/Card.tsx b/components/Member/Card.tsx
index fed8fc7..ac1bd13 100644
--- a/components/Member/Card.tsx
+++ b/components/Member/Card.tsx
@@ -1,10 +1,10 @@
import { marked } from 'marked';
import { observer } from 'mobx-react';
-import Link from 'next/link';
import { ComponentProps, FC } from 'react';
import { Member } from '../../models/Member';
import { GithubIcon } from '../Layout/Svg';
+import { TransitionLink } from '../TransitionLink';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Card } from '../ui/card';
@@ -32,14 +32,14 @@ export const MemberCard: FC = observer(
{github && (
-
+
-
+
)}
{String(nickname)}
diff --git a/components/Project/NewCard.tsx b/components/Project/NewCard.tsx
index 48b6690..5092e89 100644
--- a/components/Project/NewCard.tsx
+++ b/components/Project/NewCard.tsx
@@ -1,10 +1,10 @@
import { Project } from '@idea2app/data-server';
import { observer } from 'mobx-react';
-import Link from 'next/link';
import { FC, useContext } from 'react';
import { I18nContext } from '../../models/Translation';
import type zhCN from '../../translation/zh-CN';
+import { TransitionLink } from '../TransitionLink';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../ui/card';
@@ -43,7 +43,9 @@ export const ProjectCard: FC = observer(({ id, name, status = 0 }) => {
diff --git a/components/TransitionLink.tsx b/components/TransitionLink.tsx
new file mode 100644
index 0000000..7d91a0b
--- /dev/null
+++ b/components/TransitionLink.tsx
@@ -0,0 +1,79 @@
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { ComponentProps, MouseEvent } from 'react';
+
+type ViewTransitionDocument = Document & {
+ startViewTransition?: (updateCallback: () => Promise | unknown) => void;
+};
+
+type TransitionLinkProps = Omit, 'href'> & {
+ href: string;
+};
+
+export function TransitionLink({
+ href,
+ replace,
+ scroll,
+ shallow,
+ locale,
+ onClickCapture,
+ ...props
+}: TransitionLinkProps) {
+ const router = useRouter();
+
+ const handleClickCapture = (event: MouseEvent) => {
+ onClickCapture?.(event);
+
+ if (event.defaultPrevented) return;
+
+ if (
+ event.button !== 0 ||
+ event.metaKey ||
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.altKey ||
+ props.target ||
+ typeof href !== 'string' ||
+ !href.startsWith('/')
+ )
+ return;
+
+ const documentWithTransition = document as ViewTransitionDocument;
+
+ if (!documentWithTransition.startViewTransition) return;
+
+ const nextURL = new URL(href, location.href),
+ currentURL = new URL(router.asPath, location.href);
+
+ const isHashOnlyNavigation =
+ nextURL.pathname === currentURL.pathname &&
+ nextURL.search === currentURL.search &&
+ nextURL.hash !== currentURL.hash;
+
+ if (isHashOnlyNavigation || nextURL.href === currentURL.href)
+ return;
+
+ event.preventDefault();
+
+ const navigate = () =>
+ replace
+ ? router.replace(href, undefined, { scroll, shallow, locale })
+ : router.push(href, undefined, { scroll, shallow, locale });
+
+ documentWithTransition.startViewTransition(() => navigate());
+ };
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 1a684cb..646a401 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -26,6 +26,29 @@ const organizationJsonLd = {
email: 'contact@idea2app.cn',
};
+const speculationRules = {
+ prerender: [
+ {
+ where: {
+ or: [{ href_matches: '/member*' }, { href_matches: '/open-source*' }],
+ },
+ eagerness: 'moderate',
+ },
+ ],
+ prefetch: [
+ {
+ where: {
+ or: [
+ { href_matches: '/member*' },
+ { href_matches: '/open-source*' },
+ { href_matches: '/project*' },
+ ],
+ },
+ eagerness: 'moderate',
+ },
+ ],
+};
+
interface CustomDocumentProps {
language: LanguageCode;
colorScheme: ColorScheme;
@@ -67,6 +90,10 @@ export default class CustomDocument extends Document {
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=attach_file_add,chat,code,dark_mode,diversity_3,fingerprint,key,keyboard_arrow_down,language,light_mode,menu,translate,trending_up,visibility&display=swap"
/>
+
diff --git a/pages/index.tsx b/pages/index.tsx
index 5c6501d..4d423ab 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -5,6 +5,7 @@ import { FC, useContext } from 'react';
import { Minute, Second } from 'web-utility';
import { Button } from '@/components/ui/button';
+import { TransitionLink } from '@/components/TransitionLink';
import { PartnerOverview } from '../components/Client/Partner';
import { GitListLayout } from '../components/Git';
import { SymbolIcon } from '../components/Icon';
@@ -47,18 +48,21 @@ export const getStaticProps: GetStaticProps = async () => {
const HomePage: FC = observer(({ repositories, members }) => {
const i18n = useContext(I18nContext);
- const { t } = i18n;
+ const { t } = i18n,
+ partners = PARTNERS_INFO(i18n);
return (
-
-
-
+
+
+
+
+
-
+
{t('idea2app_summary')}
{t('idea2app_slogan')}
@@ -70,7 +74,7 @@ const HomePage: FC = observer(({ repositories, members }) => {
{service(i18n).map(({ title, summary, icon, buttonText, buttonLink }) => (
@@ -80,13 +84,13 @@ const HomePage: FC = observer(({ repositories, members }) => {
{summary}
))}
@@ -95,26 +99,28 @@ const HomePage: FC = observer(({ repositories, members }) => {
-
- {/**
- * @todo: polish the carousel animation
- */}
- {Array.from({ length: 3 }).map((_, index) => (
- -
- {PARTNERS_INFO(i18n).map(({ name, ...rest }) => (
-
- ))}
-
- ))}
-
+
+
+ {/**
+ * @todo: polish the carousel animation
+ */}
+ {Array.from({ length: 2 }).map((_, index) => (
+ -
+ {partners.map(({ name, ...rest }) => (
+
+ ))}
+
+ ))}
+
+
-
+
{members.map(item => (
-
@@ -127,7 +133,9 @@ const HomePage: FC = observer(({ repositories, members }) => {
);
diff --git a/styles/main.css b/styles/main.css
index 5494951..9762019 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -30,4 +30,106 @@ div:has(.next-error-h1) {
@utility scrollbar-none {
scrollbar-width: none;
-}
\ No newline at end of file
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ html {
+ scroll-behavior: smooth;
+ }
+
+ ::view-transition-old(root),
+ ::view-transition-new(root) {
+ animation-duration: 0.35s;
+ animation-timing-function: ease;
+ }
+
+ ::view-transition-old(root) {
+ animation-name: fade-out;
+ }
+
+ ::view-transition-new(root) {
+ animation-name: fade-in;
+ }
+
+ .landing-hero-logo,
+ .landing-hero-copy,
+ .landing-service-card,
+ .landing-section {
+ animation: rise-in 0.7s ease both;
+ }
+
+ .landing-hero-copy {
+ animation-delay: 0.08s;
+ }
+
+ .landing-service-card:nth-child(2) {
+ animation-delay: 0.12s;
+ }
+
+ .landing-service-card:nth-child(3) {
+ animation-delay: 0.18s;
+ }
+
+ .partner-marquee-track {
+ animation: carousel-scroll 24s linear infinite;
+ }
+
+ .partner-marquee:hover .partner-marquee-track {
+ animation-play-state: paused;
+ }
+
+ .landing-service-card {
+ transition:
+ transform 0.25s ease,
+ box-shadow 0.25s ease,
+ background-color 0.25s ease;
+ }
+
+ .landing-service-card:hover {
+ transform: translateY(-0.375rem);
+ box-shadow: 0 1rem 2.5rem color-mix(in srgb, currentColor 10%, transparent);
+ }
+
+}
+
+@keyframes rise-in {
+ from {
+ opacity: 0;
+ transform: translateY(1.5rem);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes fade-out {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes carousel-scroll {
+ from {
+ transform: translateX(0);
+ }
+
+ to {
+ transform: translateX(-50%);
+ }
+}