mirror of
https://github.com/DefectingCat/DefectingCat.github.io
synced 2025-07-16 09:11:38 +00:00
Add app directory
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -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
29
app/layout.tsx
Normal 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
32
app/page.tsx
Normal 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
22
app/theme-provider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
|
@ -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;
|
|
@ -11,7 +11,8 @@ const nextConfig = {
|
|||||||
images: isExport ? { unoptimized: true } : {},
|
images: isExport ? { unoptimized: true } : {},
|
||||||
experimental: {
|
experimental: {
|
||||||
// runtime: 'experimental-edge',
|
// runtime: 'experimental-edge',
|
||||||
largePageDataBytes: 512 * 1000,
|
// largePageDataBytes: 512 * 1000,
|
||||||
|
appDir: true,
|
||||||
},
|
},
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: process.env.NODE_ENV === 'production',
|
removeConsole: process.env.NODE_ENV === 'production',
|
||||||
|
40
package.json
40
package.json
@ -14,20 +14,20 @@
|
|||||||
"pretty": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown"
|
"pretty": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,md,mdx,css}\" --ignore-unknown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codesandbox/sandpack-react": "^1.20.6",
|
"@codesandbox/sandpack-react": "^2.1.9",
|
||||||
"@docsearch/react": "3",
|
"@docsearch/react": "3",
|
||||||
"@giscus/react": "^2.2.6",
|
"@giscus/react": "^2.2.8",
|
||||||
"@mapbox/rehype-prism": "^0.8.0",
|
"@mapbox/rehype-prism": "^0.8.0",
|
||||||
"@tweenjs/tween.js": "^18.6.4",
|
"@tweenjs/tween.js": "^18.6.4",
|
||||||
"algoliasearch": "^4.14.3",
|
"algoliasearch": "^4.15.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"next": "13.1.6",
|
"next": "13.2.4",
|
||||||
"next-mdx-remote": "^4.3.0",
|
"next-mdx-remote": "^4.4.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.8.0",
|
||||||
"rehype-react": "^7.1.2",
|
"rehype-react": "^7.1.2",
|
||||||
"rehype-slug": "^5.1.0",
|
"rehype-slug": "^5.1.0",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
@ -37,29 +37,29 @@
|
|||||||
"rua-three": "^1.1.1",
|
"rua-three": "^1.1.1",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
"stats.js": "^0.17.0",
|
"stats.js": "^0.17.0",
|
||||||
"three": "^0.149.0",
|
"three": "^0.150.1",
|
||||||
"unified": "^10.1.2"
|
"unified": "^10.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.15.3",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.28",
|
||||||
"@types/stats.js": "^0.17.0",
|
"@types/stats.js": "^0.17.0",
|
||||||
"@types/three": "^0.148.1",
|
"@types/three": "^0.149.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.14",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.1.6",
|
"eslint-config-next": "13.2.4",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"jest": "^29.4.1",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.4.1",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.5",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "4.9.4"
|
"typescript": "5.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
207
pages/about.tsx
207
pages/about.tsx
@ -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;
|
|
@ -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' });
|
|
||||||
}
|
|
@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
132
pages/g/[id].tsx
132
pages/g/[id].tsx
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
187
pages/index.tsx
187
pages/index.tsx
@ -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;
|
|
@ -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;
|
|
@ -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
1317
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@ -15,8 +19,20 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
// "typeRoots": ["./types", "./node_modules/@types"],
|
// "typeRoots": ["./types", "./node_modules/@types"],
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user