Add app directory

This commit is contained in:
DefectingCat
2023-03-20 16:00:43 +08:00
parent 01c6ab55c2
commit f0f9e620b5
23 changed files with 836 additions and 1987 deletions

View File

@ -1,3 +1,5 @@
{
"svn.ignoreMissingSvnWarning": true
}
"svn.ignoreMissingSvnWarning": true,
"typescript.tsdk": "node_modules/.pnpm/typescript@5.0.2/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

29
app/layout.tsx Normal file
View File

@ -0,0 +1,29 @@
import 'styles/globals.css';
import RUAThemeProvider from './theme-provider';
export const metadata = {
title: 'RUA',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
rel="preconnect"
href="https://ZUYZBUJBQW-dsn.algolia.net"
crossOrigin=""
/>
</head>
<body>
<RUAThemeProvider>{children}</RUAThemeProvider>
</body>
</html>
);
}

32
app/page.tsx Normal file
View File

@ -0,0 +1,32 @@
import clsx from 'clsx';
import { gltfLoader, manager } from 'lib/gltf-loader';
import { getMousePosition } from 'lib/utils';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import Image from 'next/image';
import { Suspense, useCallback } from 'react';
import { InitFn, THREE, useThree } from 'rua-three';
import styles from 'styles/index/index.module.css';
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
export default function Page() {
return (
<main className="h-[calc(100vh-142px)] flex justify-center items-center text-xl">
<div className="z-0 flex flex-col w-full h-full max-w-4xl px-4 py-32 text-2xl">
<h1 className="flex pb-4 text-5xl">
<span className={clsx('font-Aleo font-semibold', styles.gradient)}>
Hi there
</span>
<span className="ml-3">
<Image
src="/images/img/hands.svg"
alt="hands"
width={36}
height={36}
/>
</span>
</h1>
</div>
</main>
);
}

22
app/theme-provider.tsx Normal file
View File

@ -0,0 +1,22 @@
'use client';
import { ThemeProvider } from 'next-themes';
import { ReactNode } from 'react';
export default function RUAThemeProvider({
children,
}: {
children: ReactNode;
}) {
return (
<ThemeProvider
attribute="class"
storageKey="rua-theme"
themes={['light', 'dark']}
enableSystem
defaultTheme="system"
>
{children}
</ThemeProvider>
);
}

View File

@ -1,24 +0,0 @@
import clsx from 'clsx';
type Props = {
children: React.ReactElement | React.ReactElement[];
};
const BlogList = ({ children }: Props) => {
return (
<>
<h1
className={clsx(
'text-5xl font-bold text-center font-Barlow',
'mt-8 mb-20 text-gray-800 dark:text-gray-200'
)}
>
Blog posts
</h1>
<div className="px-4 lg:px-0">{children}</div>
</>
);
};
export default BlogList;

View File

@ -1,22 +0,0 @@
import dynamic from 'next/dynamic';
const Footer = dynamic(() => import('components/footer'));
const HeadBar = dynamic(() => import('components/nav-bar'));
const BackToTop = dynamic(() => import('components/common/back-to-top'));
type Props = {
children: React.ReactNode;
};
const MainLayout = ({ children }: Props) => {
return (
<>
<HeadBar />
{children}
<Footer />
<BackToTop />
</>
);
};
export default MainLayout;

View File

@ -11,7 +11,8 @@ const nextConfig = {
images: isExport ? { unoptimized: true } : {},
experimental: {
// runtime: 'experimental-edge',
largePageDataBytes: 512 * 1000,
// largePageDataBytes: 512 * 1000,
appDir: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',

View File

@ -14,20 +14,20 @@
"pretty": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown"
},
"dependencies": {
"@codesandbox/sandpack-react": "^1.20.6",
"@codesandbox/sandpack-react": "^2.1.9",
"@docsearch/react": "3",
"@giscus/react": "^2.2.6",
"@giscus/react": "^2.2.8",
"@mapbox/rehype-prism": "^0.8.0",
"@tweenjs/tween.js": "^18.6.4",
"algoliasearch": "^4.14.3",
"algoliasearch": "^4.15.0",
"dayjs": "^1.11.7",
"next": "13.1.6",
"next-mdx-remote": "^4.3.0",
"next": "13.2.4",
"next-mdx-remote": "^4.4.1",
"next-themes": "^0.2.1",
"octokit": "^2.0.14",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.7.1",
"react-icons": "^4.8.0",
"rehype-react": "^7.1.2",
"rehype-slug": "^5.1.0",
"remark-frontmatter": "^4.0.1",
@ -37,29 +37,29 @@
"rua-three": "^1.1.1",
"sharp": "^0.31.3",
"stats.js": "^0.17.0",
"three": "^0.149.0",
"three": "^0.150.1",
"unified": "^10.1.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/jest": "^29.4.0",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.0",
"@types/node": "18.15.3",
"@types/react": "18.0.28",
"@types/stats.js": "^0.17.0",
"@types/three": "^0.148.1",
"autoprefixer": "^10.4.13",
"@types/three": "^0.149.0",
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"cross-env": "^7.0.3",
"dotenv": "^16.0.3",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"gray-matter": "^4.0.3",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"postcss": "^8.4.21",
"prettier": "^2.8.3",
"tailwindcss": "^3.2.4",
"typescript": "4.9.4"
"prettier": "^2.8.5",
"tailwindcss": "^3.2.7",
"typescript": "5.0.2"
}
}

View File

@ -1,53 +0,0 @@
import useRouterLoading from 'lib/hooks/use-router-loading';
import { ThemeProvider } from 'next-themes';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import { Suspense, useEffect } from 'react';
import 'styles/globals.css';
import 'styles/prism-one-dark.css';
import 'styles/prism-one-light.css';
import 'styles/rua.css';
import { AppPropsWithLayout } from 'types';
const VercelLoading = dynamic(
() => import('components/rua/loading/vercel-loading'),
{
suspense: true,
}
);
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
const { loading } = useRouterLoading();
useEffect(() => {
document.body.style.transition = 'all 0.3s ease-out';
}, []);
return (
<>
<Head>
<link rel="shortcut icon" href="/images/favicon.ico" />
<meta name="keywords" content="Blog RUA" />
<meta name="description" content="Personal blog." />
<meta name="author" content="Arthur,i@rua.plus" />
<title>RUA</title>
</Head>
<ThemeProvider
attribute="class"
storageKey="rua-theme"
themes={['light', 'dark']}
enableSystem
defaultTheme="system"
>
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
<Suspense fallback>{loading && <VercelLoading />}</Suspense>
</>
);
}
export default MyApp;

View File

@ -1,30 +0,0 @@
import { Head, Html, Main, NextScript } from 'next/document';
import { getSandpackCssText } from '@codesandbox/sandpack-react';
export default function Document() {
return (
<Html lang="en">
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Aleo&family=Aref+Ruqaa&family=Barlow:ital@0;1&family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500;1,600&family=Poppins:ital@0;1&display=swap"
rel="stylesheet"
></link>
<link
rel="preconnect"
href="https://ZUYZBUJBQW-dsn.algolia.net"
crossOrigin=""
/>
<style
dangerouslySetInnerHTML={{ __html: getSandpackCssText() }}
id="sandpack"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@ -1,207 +0,0 @@
import TWEEN from '@tweenjs/tween.js';
import clsx from 'clsx';
import MainLayout from 'layouts/common/main-layout';
import { gltfLoader, manager } from 'lib/gltf-loader';
import { getMousePosition } from 'lib/utils';
import { useTheme } from 'next-themes';
import dynamic from 'next/dynamic';
import { Suspense, useEffect, useRef, useState } from 'react';
import { InitFn, THREE, useThree } from 'rua-three';
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { NextPageWithLayout } from 'types';
const Loading = dynamic(() => import('components/rua/loading/rua-loading'), {
suspense: true,
});
const rotationY = 0.4;
const rotationX = 0.2;
const About: NextPageWithLayout = () => {
const [loading, setLoading] = useState(true);
const [showLoading, setShowLoading] = useState(true);
manager.onLoad = () => {
setLoading(false);
setTimeout(() => {
setShowLoading(false);
}, 300);
};
// After model loading, set theme to dark mode.
const restore = useRef(false);
const mounted = useRef(false);
const { systemTheme, theme, setTheme } = useTheme();
const currentTheme = theme === 'system' ? systemTheme : theme;
// setDarkMode is async called by setTimeout, when component is unmounted
// it should not be called.
const setDarkMode = () => {
if (!showLoading) return;
if (currentTheme === 'dark') return;
if (!mounted.current) return;
restore.current = true;
document.body.style.transition = 'all 1.2s ease-out';
setTheme('dark');
};
useEffect(
() => {
mounted.current = true;
return () => {
mounted.current = false;
if (!restore.current) return;
setTheme('light');
document.body.style.transition = 'all 0.3s ease-out';
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const init: InitFn = ({
scene,
controls,
camera,
isOrbitControls,
frameArea,
isPerspectiveCamera,
addRenderCallback,
addWindowEvent,
}) => {
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
if (isOrbitControls(controls)) {
controls.enablePan = false;
controls.enableZoom = false;
controls.enableRotate = false;
}
const handleLoad = (gltf: GLTF) => {
const root = gltf.scene;
scene.add(root);
const clock = new THREE.Clock();
const mixer = new THREE.AnimationMixer(root);
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
addRenderCallback(() => {
mixer.update(clock.getDelta());
});
const box = new THREE.Box3().setFromObject(root);
const boxSize = box.getSize(new THREE.Vector3()).length();
const boxCenter = box.getCenter(new THREE.Vector3());
if (isPerspectiveCamera(camera)) {
frameArea(boxSize, boxSize, boxCenter, camera);
}
controls.target.copy(boxCenter);
controls.update();
// Rotate 180 degress
root.rotation.y = Math.PI * 2;
// Enter animation
const entryValue = {
rotationY: root.rotation.y,
meshY: root.position.y,
cameraY: camera.position.y,
z: camera.position.z,
};
const enter = new TWEEN.Tween(entryValue)
.to(
{
rotationY: 0,
meshY: entryValue.meshY - 1,
cameraY: entryValue.cameraY + 0.5,
z: entryValue.z - 16,
},
1200
)
.onUpdate((obj) => {
// root.rotation.y = obj.rotationY;
root.position.y = obj.meshY;
camera.position.y = obj.cameraY;
camera.position.z = obj.z;
})
.easing(TWEEN.Easing.Circular.Out)
.onComplete(() => {
document.body.style.transition = 'all 0.3s ease-out';
});
setTimeout(() => {
enter.start();
setDarkMode();
}, 1000);
// Render animation
addRenderCallback((time) => {
TWEEN.update(time / 0.001);
});
const halfWidth = Math.floor(window.innerWidth / 2);
const halfHeight = Math.floor(window.innerHeight / 2);
const updateMousePosition = (e: MouseEvent | globalThis.TouchEvent) => {
const { x, y } = getMousePosition(e);
// > 0 is right, < 0 is left
// if (directionX > 0) root.rotation.y += 0.01;
root.rotation.y = rotationY * ((x - halfWidth) / halfWidth);
root.rotation.x = rotationX * ((y - halfHeight) / halfHeight);
};
addWindowEvent('mousemove', updateMousePosition, {
passive: true,
});
addWindowEvent('touchmove', updateMousePosition, {
passive: true,
});
};
gltfLoader.load('./models/cloud_station/modelDraco.gltf', handleLoad);
};
const { ref } = useThree({
init,
alpha: true,
});
return (
<>
<div className="fixed top-0 left-0 -z-10">
<canvas ref={ref} className="w-full h-full"></canvas>
<Suspense fallback>
{showLoading && (
<div
className={clsx(
'absolute top-0 left-0',
'items-center flex justify-center',
'w-full h-full transition-all duration-500',
'bg-white dark:bg-rua-gray-900',
loading ? 'opacity-1' : 'opacity-0'
)}
>
<Loading />
</div>
)}
</Suspense>
</div>
<main className="h-[calc(100vh-142px)] flex flex-col">
<div
className={clsx(
'flex max-w-3xl',
'px-10 py-4 mx-auto lg:px-0 lg:py-10'
)}
>
<h1 className="text-5xl font-semibold font-Barlow">About</h1>
</div>
</main>
</>
);
};
About.getLayout = function getLayout(page) {
return <MainLayout>{page}</MainLayout>;
};
export default About;

View File

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' });
}

View File

@ -1,20 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Post } from 'types';
import { postLists } from 'lib/posts';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Post[]>
) {
const getPosts = async () => {
const posts = await postLists();
res.status(200).json(posts);
};
switch (req.method) {
case 'GET':
return getPosts();
default:
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -1,91 +0,0 @@
import PostCardLoading from 'components/rua/loading/post-card-loading';
import MainLayout from 'layouts/common/main-layout';
import { getPostListPath, postLists, PostPerPage } from 'lib/posts';
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
import dynamic from 'next/dynamic';
import { Fragment, ReactElement, Suspense } from 'react';
import { Post } from 'types';
const PostCard = dynamic(() => import('components/post-card'), {
suspense: true,
});
const BlogList = dynamic(() => import('layouts/common/blog-list'), {
suspense: true,
});
const Pagination = dynamic(() => import('components/rua/rua-pagination'), {
suspense: true,
});
const BlogPage = ({
posts,
prev,
next,
total,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
<main className="max-w-4xl mx-auto">
<Suspense fallback>
<BlogList>
{posts.map((post) => (
<Fragment key={post.slug}>
<Suspense fallback={<PostCardLoading />}>
<PostCard post={post} />
</Suspense>
</Fragment>
))}
</BlogList>
</Suspense>
<Suspense fallback>
<Pagination
className="py-6 mt-4 px-7 lg:px-5"
hasPrev={!!prev}
hasNext={next <= total}
prevLink={prev === 1 ? '/blog' : `/blog/${prev}`}
nextLink={`/blog/${next}`}
current={next - 1}
total={total}
/>
</Suspense>
</main>
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: await getPostListPath(),
fallback: false,
};
};
export const getStaticProps: GetStaticProps<{
posts: Post[];
prev: number;
next: number;
total: number;
}> = async ({ params }) => {
const page = Number(params?.page);
if (!page) {
return {
notFound: true,
};
}
const posts = await postLists();
return {
props: {
posts: posts.slice((page - 1) * PostPerPage, PostPerPage * page),
prev: page - 1,
next: page + 1,
total: Math.ceil(posts.length / PostPerPage),
},
};
};
BlogPage.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default BlogPage;

View File

@ -1,75 +0,0 @@
import PostCardLoading from 'components/rua/loading/post-card-loading';
import MainLayout from 'layouts/common/main-layout';
import { postLists, PostPerPage } from 'lib/posts';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import dynamic from 'next/dynamic';
import { Fragment, ReactElement, Suspense } from 'react';
import { Post } from 'types';
const PostCard = dynamic(() => import('components/post-card'), {
suspense: true,
});
const BlogList = dynamic(() => import('layouts/common/blog-list'), {
suspense: true,
});
const Pagination = dynamic(() => import('components/rua/rua-pagination'), {
suspense: true,
});
const Blog = ({
posts,
next,
total,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
<main className="max-w-4xl mx-auto">
<Suspense fallback>
<BlogList>
{posts.map((post) => (
<Fragment key={post.slug}>
<Suspense fallback={<PostCardLoading />}>
<PostCard post={post} />
</Suspense>
</Fragment>
))}
</BlogList>
</Suspense>
<Suspense fallback>
<Pagination
className="py-6 mt-4 px-7 lg:px-5"
hasPrev={false}
hasNext={next <= total}
prevLink={''}
nextLink={`/blog/${next}`}
current={1}
total={total}
/>
</Suspense>
</main>
</>
);
};
export const getStaticProps: GetStaticProps<{
posts: Post[];
next: number;
total: number;
}> = async () => {
const posts = await postLists();
return {
props: {
// Latest posts.
posts: posts.slice(0, PostPerPage),
next: 2,
total: Math.ceil(posts.length / PostPerPage),
},
};
};
Blog.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Blog;

View File

@ -1,132 +0,0 @@
import LinkAnchor from 'components/mdx/link-anchor';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import MainLayout from 'layouts/common/main-layout';
import { getSignalGist, SingalGist } from 'lib/fetcher';
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
import dynamic from 'next/dynamic';
import Image from 'next/image';
import Link from 'next/link';
import avatar from 'public/images/img/avatar.svg';
import { Fragment, ReactElement, Suspense } from 'react';
import { useRouter } from 'next/router';
const GistsCode = dynamic(() => import('components/gists/gists-code'), {
suspense: true,
});
const GistsSkeleton = dynamic(() => import('components/gists/gists-skeleton'), {
suspense: true,
});
dayjs.extend(relativeTime);
const Gist = ({ gist }: InferGetStaticPropsType<typeof getStaticProps>) => {
const router = useRouter();
if (router.isFallback) {
return (
<Suspense fallback>
<GistsSkeleton />
</Suspense>
);
}
return (
<>
<main className="max-w-5xl px-4 mx-auto lg:px-0">
<div className="pb-4 text-sm">
<div className="flex items-center py-1 ">
<Image
src={avatar}
alt="Avatar"
priority
width={32}
height={32}
className="rounded-full "
/>
<h1 className="ml-2 overflow-hidden text-xl whitespace-nowrap overflow-ellipsis">
<Link href="/gists">
<LinkAnchor external={false}>{gist.login}</LinkAnchor>
</Link>
/{Object.keys(gist.files)[0]}
</h1>
</div>
<p className="pl-10 text-gray-400 ">
Last active: {dayjs(gist.updated_at).fromNow()}
</p>
<div className="py-4">
<p className="pb-2 text-lg text-gray-500">{gist.description}</p>
{Object.keys(gist.files).map((f) => (
<Fragment key={gist.files[f].raw_url}>
<Suspense fallback>
<GistsCode file={gist.files[f]} showFileName />
</Suspense>
</Fragment>
))}
</div>
</div>
</main>
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
// const result = await getGists();
// const last = Number(result?.pageSize.last);
// const paths: (
// | string
// | {
// params: ParsedUrlQuery;
// locale?: string | undefined;
// }
// )[] = [];
// for (let i = 1; i <= last; i++) {
// const result = await getGists(i);
// paths.push(...(result?.gists.map((g) => ({ params: { id: g.id } })) ?? []));
// }
return {
paths: [],
fallback: true,
};
};
export const getStaticProps: GetStaticProps<{
id: string | undefined;
gist: SingalGist;
}> = async ({ params }) => {
if (typeof params?.id !== 'string') {
return {
notFound: true,
};
}
try {
const gist = await getSignalGist(params.id);
if (!gist || !gist.files) {
return {
notFound: true,
};
}
return {
props: {
id: params?.id?.toString(),
gist,
},
revalidate: 600,
};
} catch (err) {
return {
notFound: true,
};
}
};
Gist.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Gist;

View File

@ -1,125 +0,0 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import MainLayout from 'layouts/common/main-layout';
import { GetGists, getGists, GetUser, getUser } from 'lib/fetcher';
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
import dynamic from 'next/dynamic';
import { ReactElement, Suspense } from 'react';
import { useRouter } from 'next/router';
const UserInfo = dynamic(() => import('components/gists/user-info'), {
suspense: true,
});
const FileContent = dynamic(() => import('components/gists/file-content'), {
suspense: true,
});
const Pagination = dynamic(() => import('components/rua/rua-pagination'), {
suspense: true,
});
dayjs.extend(relativeTime);
const Gists = ({
gists,
user,
prev,
next,
total,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
const router = useRouter();
if (router.isFallback) {
return <>Loading...</>;
}
return (
<>
<main className="max-w-5xl px-4 mx-auto lg:px-0">
<div className="md:flex">
<Suspense fallback>
<UserInfo user={user} />
</Suspense>
<div className="flex-1 px-1 py-4 overflow-hidden md:pl-8">
<Suspense fallback>
<FileContent gists={gists.gists} />
<Pagination
className="mt-4"
hasPrev={!!prev}
hasNext={!!next}
prevLink={prev === 1 ? `/gists/` : `/gists/${prev}`}
nextLink={`/gists/${next}`}
current={prev == null ? next - 1 : prev + 1}
total={total}
/>
</Suspense>
</div>
</div>
</main>
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
// const result = await getGists();
// const next = Number(result?.pageSize.next);
// const last = Number(result?.pageSize.last);
// const paths: (
// | string
// | {
// params: ParsedUrlQuery;
// locale?: string | undefined;
// }
// )[] = [];
// for (let i = next; i <= last; i++) {
// paths.push({
// params: {
// p: i.toString(),
// },
// });
// }
return {
paths: [],
fallback: true,
};
};
export const getStaticProps: GetStaticProps<{
gists: GetGists;
user: GetUser;
prev: number;
next: number;
total: number;
}> = async ({ params }) => {
if (typeof params?.p !== 'string') {
return {
notFound: true,
};
}
const result = await getGists(Number(params?.p));
if (!result) {
return {
notFound: true,
};
}
const user = await getUser();
return {
props: {
gists: result,
user,
prev: Number(result.pageSize.prev),
next: Number(result.pageSize.next),
total: Number(result.pageSize.last),
},
revalidate: 600,
};
};
Gists.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Gists;

View File

@ -1,87 +0,0 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import MainLayout from 'layouts/common/main-layout';
import { GetGists, getGists, GetUser, getUser } from 'lib/fetcher';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import dynamic from 'next/dynamic';
import { ReactElement, Suspense } from 'react';
const UserInfo = dynamic(() => import('components/gists/user-info'), {
suspense: true,
});
const FileContent = dynamic(() => import('components/gists/file-content'), {
suspense: true,
});
const Pagination = dynamic(() => import('components/rua/rua-pagination'), {
suspense: true,
});
dayjs.extend(relativeTime);
const Gists = ({
gists,
user,
prev,
next,
total,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
<main className="max-w-5xl px-4 mx-auto lg:px-0">
<div className="md:flex">
<Suspense fallback>
<UserInfo user={user} />
</Suspense>
<div className="flex-1 px-1 py-4 overflow-hidden md:pl-8">
<Suspense fallback>
<FileContent gists={gists.gists} />
<Pagination
className="mt-4"
hasPrev={!!prev}
hasNext={!!next}
prevLink={prev === 1 ? `/gists/` : `/gists/${prev}`}
nextLink={`/gists/${next}`}
current={prev == null ? next - 1 : prev + 1}
total={total}
/>
</Suspense>
</div>
</div>
</main>
</>
);
};
export const getStaticProps: GetStaticProps<{
gists: GetGists;
user: GetUser;
prev: number;
next: number;
total: number;
}> = async () => {
const result = await getGists();
if (!result)
return {
notFound: true,
};
const user = await getUser();
return {
props: {
gists: result,
user,
prev: Number(result.pageSize.prev),
next: Number(result.pageSize.next),
total: Number(result.pageSize.last),
},
revalidate: 600,
};
};
Gists.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Gists;

View File

@ -1,187 +0,0 @@
import clsx from 'clsx';
import MainLayout from 'layouts/common/main-layout';
import { gltfLoader, manager } from 'lib/gltf-loader';
import { getMousePosition } from 'lib/utils';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import Image from 'next/image';
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { InitFn, THREE, useThree } from 'rua-three';
import styles from 'styles/index/index.module.css';
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import type { NextPageWithLayout } from 'types';
const Loading = dynamic(() => import('components/rua/loading/rua-loading'), {
suspense: true,
});
const rotationY = 0.4;
const rotationX = 0.18;
const Home: NextPageWithLayout = () => {
const wrapper = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({
width: 500,
height: 300,
});
const [loading, setLoading] = useState(true);
const [showLoading, setShowLoading] = useState(true);
manager.onLoad = () => {
setLoading(false);
setTimeout(() => {
setShowLoading(false);
}, 300);
};
const setCanvasSize = useCallback(() => {
if (!wrapper.current) return;
const width = wrapper.current.clientWidth;
const height = wrapper.current.clientHeight;
setSize({
width,
height,
});
}, []);
useEffect(() => {
setCanvasSize();
window.addEventListener('resize', setCanvasSize);
return () => {
window.removeEventListener('resize', setCanvasSize);
};
}, [setCanvasSize]);
const init: InitFn = ({
scene,
camera,
controls,
frameArea,
isOrbitControls,
isPerspectiveCamera,
addWindowEvent,
addRenderCallback,
}) => {
if (isOrbitControls(controls)) {
controls.enableRotate = false;
controls.enablePan = false;
controls.enableZoom = false;
controls.minDistance = 1;
controls.minPolarAngle = Math.PI * 0.2;
controls.maxPolarAngle = Math.PI * 0.5;
controls.maxAzimuthAngle = Math.PI * 0.2;
}
const light = new THREE.SpotLight(0xffffff, 2, 100, 15);
scene.add(new THREE.AmbientLight(0xffffff, 1));
scene.add(light);
const handleLoad = (gltf: GLTF) => {
const root = gltf.scene;
scene.add(root);
const clock = new THREE.Clock();
const mixer = new THREE.AnimationMixer(root);
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
addRenderCallback(() => {
mixer.update(clock.getDelta());
});
const box = new THREE.Box3().setFromObject(root);
const boxSize = box.getSize(new THREE.Vector3()).length();
const boxCenter = box.getCenter(new THREE.Vector3());
light.target = root;
light.position.set(0, 2, 6);
light.rotateX(Math.PI * 0.4);
isPerspectiveCamera(camera) &&
frameArea(boxSize * 0.8, boxSize, boxCenter, camera);
controls.target.copy(boxCenter);
controls.update();
root.position.y += 0.1;
camera.position.z -= 0.2;
const halfWidth = Math.floor(window.innerWidth / 2);
const halfHeight = Math.floor(window.innerHeight / 2);
const updateMousePosition = (e: MouseEvent | globalThis.TouchEvent) => {
const { x, y } = getMousePosition(e);
// > 0 is right, < 0 is left
// if (directionX > 0) root.rotation.y += 0.01;
root.rotation.y = rotationY * ((x - halfWidth) / halfWidth);
root.rotation.x = rotationX * ((y - halfHeight) / halfHeight);
};
addWindowEvent('mousemove', updateMousePosition, {
passive: true,
});
addWindowEvent('touchmove', updateMousePosition, {
passive: true,
});
};
gltfLoader.load('./models/just_a_hungry_cat/modelDraco.gltf', handleLoad);
};
const { ref } = useThree({
init,
...size,
alpha: true,
});
return (
<>
<Head>
<title>RUA - HOME</title>
</Head>
<main className="h-[calc(100vh-142px)] flex justify-center items-center text-xl">
<div className="z-0 flex flex-col w-full h-full max-w-4xl px-4 py-32 text-2xl">
<h1 className="flex pb-4 text-5xl">
<span className={clsx('font-Aleo font-semibold', styles.gradient)}>
Hi there
</span>
<span className="ml-3">
<Image
src="/images/img/hands.svg"
alt="hands"
width={36}
height={36}
/>
</span>
</h1>
<div
className="relative flex-1 overflow-hidden rounded-xl"
ref={wrapper}
>
<canvas ref={ref} className="absolute top-0 left-0"></canvas>
<Suspense fallback>
{showLoading && (
<div
className={clsx(
'absolute top-0 left-0 z-10',
'items-center flex justify-center',
'w-full h-full transition-all duration-500',
'bg-white',
loading ? 'opacity-1' : 'opacity-0'
)}
>
<Loading />
</div>
)}
</Suspense>
</div>
</div>
</main>
</>
);
};
Home.getLayout = function getLayout(page) {
return <MainLayout>{page}</MainLayout>;
};
export default Home;

View File

@ -1,102 +0,0 @@
import rehypePrism from '@mapbox/rehype-prism';
import components from 'components/mdx/components';
import PostCommnetLine from 'components/post/post-commnet-line';
import PostToc from 'components/post/post-toc';
import data from 'content/mdx-data';
import MainLayout from 'layouts/common/main-layout';
import useInView from 'lib/hooks/use-in-view';
import { allPostsPath, readSinglePost } from 'lib/posts';
import { generateToc, SingleToc } from 'lib/utils';
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote';
import { serialize } from 'next-mdx-remote/serialize';
import dynamic from 'next/dynamic';
import { ReactElement, Suspense } from 'react';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
const PostComment = dynamic(() => import('components/post/post-comment'), {
suspense: true,
});
const Slug = ({
mdxSource,
toc,
tocLength,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
const { ref, inView } = useInView();
return (
<>
<main id="article" className="relative max-w-4xl px-4 mx-auto my-10">
<h1>{mdxSource.frontmatter?.title}</h1>
<time>{mdxSource.frontmatter?.date}</time>
<PostToc toc={toc} tocLength={tocLength} />
<article id="post-content">
<MDXRemote {...mdxSource} components={components as {}} />
<PostCommnetLine />
<div ref={ref} className="mt-4">
<Suspense fallback>{inView && <PostComment />}</Suspense>
</div>
</article>
</main>
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: await allPostsPath(),
fallback: false,
};
};
export const getStaticProps: GetStaticProps<{
mdxSource: MDXRemoteSerializeResult;
toc: SingleToc[];
tocLength: number;
}> = async ({ params }) => {
const slug = params?.slug?.toString();
if (!slug) {
return {
notFound: true,
};
}
const post = await readSinglePost(slug);
const toc = generateToc(post);
const calcLength = (prev: number, cur: SingleToc) => {
const childLen = cur.children.length;
return childLen ? prev + childLen + 1 : prev + 1;
};
const tocLength = toc.reduce(calcLength, 0);
const mdxSource = await serialize(post, {
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[rehypePrism, { alias: { vue: 'xml' }, ignoreMissing: true }],
rehypeSlug,
],
},
parseFrontmatter: true,
scope: data,
});
return {
props: {
mdxSource,
toc,
tocLength,
},
};
};
Slug.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Slug;

View File

@ -1,182 +0,0 @@
import clsx from 'clsx';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import dynamic from 'next/dynamic';
import { Fragment, ReactElement, Suspense } from 'react';
import {
SiGitea,
SiNextdotjs,
SiRedux,
SiThreedotjs,
SiTsnode,
SiVim,
} from 'react-icons/si';
import { VscGithubInverted } from 'react-icons/vsc';
import { HiPhoto } from 'react-icons/hi2';
import MainLayout from 'layouts/common/main-layout';
const ProjectCard = dynamic(() => import('components/pages/project-card'), {
suspense: true,
});
const iconMap = {
gitea: <SiGitea />,
nextjs: <SiNextdotjs />,
github: <VscGithubInverted />,
vim: <SiVim />,
tsnode: <SiTsnode />,
three: <SiThreedotjs />,
photos: <HiPhoto />,
redux: <SiRedux />,
};
const Projects = ({
projects,
selfHosts,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
<main className="max-w-4xl px-8 py-8 mx-auto lg:px-0">
<div>
{/* Git projects */}
<div>
<h1 className="mb-4 text-2xl">Projects</h1>
</div>
<div
className={clsx(
'grid grid-cols-1 lg:grid-cols-3',
'md:grid-cols-2 gap-5'
)}
>
{projects.map((item) => (
<Fragment key={item.id}>
<Suspense fallback>
<ProjectCard
icon={iconMap[item.icon ?? 'github']}
project={item}
/>
</Suspense>
</Fragment>
))}
</div>
</div>
<div className="mt-6">
<div>
<h1 className="mb-4 text-2xl">Seft Hosts</h1>
</div>
<div
className={clsx(
'grid grid-cols-1 lg:grid-cols-3',
'md:grid-cols-2 gap-5'
)}
>
{selfHosts.map((item) => (
<Fragment key={item.id}>
<Suspense fallback>
<ProjectCard
icon={iconMap[item.icon ?? 'github']}
project={item}
/>
</Suspense>
</Fragment>
))}
</div>
</div>
</main>
</>
);
};
export type Project = {
id: number;
icon?: keyof typeof iconMap;
name: string;
description: string;
url: string;
};
export const getStaticProps: GetStaticProps<{
projects: Project[];
selfHosts: Project[];
}> = async () => {
const projects: Project[] = [
{
id: 0,
icon: 'three',
name: '3d-globe',
description: 'A 3d globe made by three.js.',
url: 'https://github.com/DefectingCat/3d-globe',
},
{
id: 1,
icon: 'nextjs',
name: 'Blog',
description: 'This site.',
url: 'https://github.com/DefectingCat/DefectingCat.github.io',
},
{
id: 2,
icon: 'tsnode',
name: 'boring-avatars-services',
description: 'Random avatars.',
url: 'https://github.com/DefectingCat/boring-avatars-services',
},
{
id: 3,
icon: 'tsnode',
name: 'RUA DDNS',
description: 'DDNS Script for DNSPod',
url: 'https://github.com/DefectingCat/rua-ddns',
},
{
id: 4,
icon: 'vim',
name: 'Dotfiles',
description: 'Some dotfiles.',
url: 'https://github.com/DefectingCat/dotfiles',
},
{
id: 5,
icon: 'redux',
name: 'RUA-Context',
description: 'A global store for React.',
url: 'https://github.com/rua-plus/rua-context',
},
{
id: 6,
icon: 'three',
name: 'RUA-Three',
description: 'A three.js hooks for React.',
url: 'https://github.com/rua-plus/rua-three',
},
];
const selfHosts: Project[] = [
{
id: 0,
icon: 'gitea',
name: 'Gitea',
description: 'Selfhost git.',
url: 'https://git.rua.plus/',
},
{
id: 1,
icon: 'photos',
name: 'Photos',
description: 'Some photos.',
url: 'https://photos.rua.plus/browse',
},
];
return {
props: {
projects,
selfHosts,
},
};
};
Projects.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Projects;

1317
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,11 @@
"compilerOptions": {
"baseUrl": ".",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -15,8 +19,20 @@
"isolatedModules": true,
"jsx": "preserve",
// "typeRoots": ["./types", "./node_modules/@types"],
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}