ref(global) update project structure

This commit is contained in:
lumither committed 2024-06-18 13:07:40 +08:00
1 parent 2fabd8d5c0
commit 6b75c17b05
32 files changed
+67 -42

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
BLOG_ENDPOINT=git@github.com:Lumither/blog-posts.git
GIT_WORK_DIR=/home/lumither/dev/blog/blog_posts_git
BACKEND_URL=https://lumither.com/api
BACKEND_PORT=3001
DB_CONNECTION=postgresql://localhost:5432/blog
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
+42
View File
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# bun
bun.lockb
# ide related
.idea
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
function About() {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
}
export default About;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
const Archive = () => {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
};
export default Archive;
+94
View File
@@ -0,0 +1,94 @@
import React from 'react';
import { Card, CardBody, CardFooter } from '@nextui-org/card';
import { Chip } from '@nextui-org/react';
import Image from 'next/image';
import Link from 'next/link';
import { MdCalendarMonth, MdNorthEast } from 'react-icons/md';
type Props = {
post: JSON
}
function BlogPostCard({ post }: Props) {
let title = (post as any)['title'];
let sub_title = (post as any)['sub_title'];
// let summary = (post as any)['summary'];
let category = (post as any)['category'];
let tags = (post as any)['tags'];
// let last_update = new Date((post as any)['last_update']);
let first_update = new Date((post as any)['first_update']);
let post_id = (post as any)['post_id'];
let header_img = (post as any)['header_img'];
return (
<div className={ `w-full` }>
<Card as={ Link } href={ `/blog/post/${ post_id }` }
className={ `group` }
isPressable
isHoverable
>
{ header_img !== '' && <div className={ `w-full overflow h-80 relative` }>
<Image
alt={ 'cover picture' }
src={ header_img }
className={ `w-full object-cover rounded-b-none` }
style={ {
'maskImage': 'linear-gradient(black 95%, transparent)',
'backdropFilter': 'blur(20px)'
} }
fill
></Image>
</div> }
<CardBody className={ `overflow-visible` }>
<div className={ `flex flex-row justify-between` }>
<div className={ `mx-2 ${ header_img !== '' ? '-mt-8' : 'mt-2' }` }>
<Chip className={ `mb-2 z-30` } radius={ `sm` } color={ `secondary` }
variant={ `solid` }>{ category }</Chip>
<p className={ `text-4xl font-bold` }> { title }</p>
<p className={ `text-xl` }> { sub_title }</p>
</div>
<MdNorthEast className={ `mt-1 mr-2` } size={ `40px` } />
</div>
</CardBody>
{/* <CardBody className={ `transition-all duration-300 hidden group-hover:block` }>
<div className={ `mx-2` }>
<p>{ summary }</p>
</div>
</CardBody>*/ }
<CardFooter>
<div className={ `flex flex-row flex-wrap justify-between w-full` }>
<div>
<ul className={ `flex flex-row flex-wrap` }>
{
tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
</li>
))
}
</ul>
</div>
<div className={ `flex flex-row place-content-end mb-1 items-center mx-1` }>
<MdCalendarMonth />
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
{ first_update.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }
</p>
</div>
</div>
</CardFooter>
</Card>
</div>
);
}
export default BlogPostCard;
+9
View File
@@ -0,0 +1,9 @@
import axios from 'axios';
export default async function fetch_posts_list(): Promise<any> {
const response = await axios(`${ process.env.BACKEND_URL }/get_post_list`);
if (response.status !== 200) {
throw new Error(response.statusText);
}
return response.data;
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
function Layout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'flex flex-auto p-7 pl-2 md:pl-7 items-stretch justify-center' }>
{ children }
</div>
);
}
export default Layout;
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import fetch_posts_list from '@/app/blog/fetch_posts_list';
import BlogPostCard from '@/app/blog/BlogPostCard';
import { Card, CardBody } from '@nextui-org/card';
import { MotionDiv } from '@/app/public/MotionDiv';
async function Blog() {
try {
const res = await fetch_posts_list();
let posts = res['posts'];
return (
// blog list
<ul className={ 'flex flex-col items-center w-full space-y-7' }>
{ posts.map((post: JSON, key: number) => (
<li key={ key } className={ 'w-full' }>
<MotionDiv
initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
>
<BlogPostCard post={ post } />
</MotionDiv>
</li>)
) }
</ul>
);
} catch (e: any) {
return (
<Card className={ `w-full` }>
<CardBody>
Fatal: Failed to load post list.
</CardBody>
</Card>);
}
}
export default Blog;
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import Markdown from 'react-markdown';
import { Card, CardBody } from '@nextui-org/card';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import rehypeSanitize from 'rehype-sanitize';
import rehypeRaw from 'rehype-raw';
import remarkToc from 'remark-toc';
import remarkFrontmatter from 'remark-frontmatter';
import axios from 'axios';
type Props = {
id: string
}
async function Generate(props: Props) {
let response: any;
try {
response = await axios(`${ process.env.BACKEND_URL }/get_post/${ props.id }`);
} catch (e) {
return (
<Card className={ `w-full` }>
<CardBody>
Fatal: Failed to load the post.
</CardBody>
</Card>
);
}
let post_data = response.data;
return (
<div>
<Card className={ `w-full max-w-full relative` }>
<CardBody>
<Markdown
className={ `p-5 max-w-full text-justify text-pretty prose prose-sm dark:prose-invert md:prose-md lg:prose-lg` }
remarkPlugins={ [ remarkGfm, remarkToc, remarkFrontmatter ] }
rehypePlugins={ [ rehypeHighlight, rehypeRaw, rehypeSanitize ] }
>{ (post_data as any)['content'] }</Markdown>
</CardBody>
</Card>
</div>
);
}
export default Generate;
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
import Generate from '@/app/blog/post/[id]/generate';
import { MotionDiv } from '@/app/public/MotionDiv';
function BlogReader({ params }: { params: { id: string } }) {
return (
<MotionDiv
initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5 } }
>
<div className={ `w-full` }>
<Generate id={ params.id } />
</div>
</MotionDiv>
);
}
export default BlogReader;
+13
View File
@@ -0,0 +1,13 @@
@import "lxgw-wenkai-webfont/style.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: "LXGW WenKai", sans-serif;
}
pre, code {
font-family: "LXGW WenKai Mono", sans-serif;
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from 'next';
import './globals.css';
import React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
export const metadata: Metadata = {
title: 'Lumither\'s site',
description: 'a small corner on internet'
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={ `dark:bg-[#282830] min-h-screen` }>
<NextThemesProvider attribute="class" defaultTheme="dark">
<div className={ `justify-center flex flex-row w-full` }>
<Navbar />
<div className={ `flex flex-col flex-auto max-w-[1024px]` }>
{ children }
<Footer />
</div>
</div>
</NextThemesProvider>
</body>
</html>
);
}
+12
View File
@@ -0,0 +1,12 @@
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
import React from 'react';
export default function Home() {
return (
<main>
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
</main>
);
}
+12
View File
@@ -0,0 +1,12 @@
'use client';
import { NextUIProvider } from '@nextui-org/react';
import React from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NextUIProvider>
{ children }
</NextUIProvider>
);
}
+5
View File
@@ -0,0 +1,5 @@
'use client';
import { motion } from 'framer-motion';
export const MotionDiv = motion.div;
+7
View File
@@ -0,0 +1,7 @@
export type Result<T, E> = {
ok: true;
value: T;
} | {
ok: false;
error: E;
};
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
const Search = () => {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
};
export default Search;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
const Tags = () => {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
};
export default Tags;
@@ -0,0 +1,13 @@
import React from 'react';
const ConstructionPlaceHolder = () => {
return (
<div className={ `flex h-full w-full justify-center items-center` }>
<p className={ `text-2xl font-bold` }>
正在施工中...
</p>
</div>
);
};
export default ConstructionPlaceHolder;
+25
View File
@@ -0,0 +1,25 @@
import React from 'react';
import Link from 'next/link';
import { IoLogoGithub } from 'react-icons/io5';
const Footer = () => {
return (
<div className={ `p-5 mb-4` }>
<div className={ `h-1 w-20 bg-zinc-500 dark:bg-zinc-400 mb-3` }></div>
<p>
© 2024-{ new Date().getFullYear() } <Link href={ `/about` }>Lumither Tao</Link>
</p>
<p>
Powered by Next.js and Rust, built with passion and love
</p>
<Link
href={ `https://github.com/Lumither/blog` }
aria-label={ `GitHub repo for this website` }
>
<IoLogoGithub className={ `mt-1` } />
</Link>
</div>
);
};
export default Footer;
+197
View File
@@ -0,0 +1,197 @@
'use client';
import React, { useEffect, useState } from 'react';
import { Button } from '@nextui-org/react';
import NextImage from 'next/image';
import {
IoFileTrayFull,
IoHome,
IoInformationCircle,
IoLogoGithub,
IoMail,
IoPencil,
IoPricetags,
IoSearch
} from 'react-icons/io5';
import Link from 'next/link';
import ThemeSwitcher from '@/components/ThemeSwitcher';
import { usePathname } from 'next/navigation';
const entries: { display_name: string; href: string; icon: React.ReactNode }[] = [
{
icon: <IoHome size={ `20px` }></IoHome>,
display_name: 'Home',
href: '/'
},
{
icon: <IoInformationCircle size={ `20px` }></IoInformationCircle>,
display_name: 'About',
href: '/about'
},
{
icon: <IoPencil size={ `20px` }></IoPencil>,
display_name: 'Blog',
href: '/blog'
},
{
icon: <IoPricetags size={ `20px` }></IoPricetags>,
display_name: 'Tags',
href: '/tags'
},
{
icon: <IoFileTrayFull size={ `20px` }></IoFileTrayFull>,
display_name: 'Archive',
href: '/archive'
},
{
icon: <IoSearch size={ `20px` }></IoSearch>,
display_name: 'Search',
href: '/search'
}
];
const connections: { label: string, href: string, icon: React.ReactNode }[] = [
{
label: 'Github',
href: 'https://github.com/Lumither',
icon: <IoLogoGithub size={ `30px` } />
},
{
label: 'email',
href: 'mailto:lumither@outlook.com',
icon: <IoMail size={ `30px` } />
}
];
function Navbar() {
const [ scrolled, setScrolled ] = useState(false);
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 0;
setScrolled(isScrolled);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
const path = usePathname();
const [ shownPath, setShownPath ] = useState(path);
useEffect(() => {
let trimmed_path = path.split(`/`)[1];
if (trimmed_path === '') {
setShownPath('HOME');
} else {
setShownPath(trimmed_path.toUpperCase());
}
}, [ path ]);
const minMobileWidth: number = 768;
const [ isMobile, setIsMobile ] = useState(false);
useEffect(() => {
const updateMobileWidth = () => {
if (window.innerWidth <= minMobileWidth) {
setIsMobile(true);
} else {
setIsMobile(false);
}
};
updateMobileWidth();
window.addEventListener('resize', updateMobileWidth);
return () => {
window.removeEventListener('resize', updateMobileWidth);
};
}, []);
return (
<>
<div
className={ `flex flex-col w-auto md:w-full min-w-[50px] max-w-[256px] mr-0 md:mr-2 ml-2 md:ml-8 min-h-dvh pt-8 sticky` }>
<div className={ `fixed min-h-screen h-full` }>
<div className={ `hidden md:block` }>
<div className={ `flex justify-center items-center my-7` }>
<NextImage
src={ 'https://oss.lumither.com/blog/pictures/compr/1717153492846703409_avatar.webp' }
alt={ 'avatar' }
width={ 200 }
height={ 200 }
className={ 'rounded-full justify-self-center' }
priority={ true }
></NextImage>
</div>
<div>
<p className={ `font-bold text-2xl` }>Lumither Tao</p>
<p className={ `text-xl text-zinc-500 dark:text-zinc-400` }>Ad Astra</p>
</div>
</div>
<div>
<ul className={ `flex flex-col space-y-2 md:space-y-0 md:flex-row items-center my-4 md:space-x-2` }>
{
connections.map((conn, key) => (
<li key={ key }>
<Button as={ Link }
href={ conn.href }
aria-label={ conn.label }
variant={ `light` }
color={ `default` }
className={ `w-fit` }
isIconOnly
>
{ conn.icon }
</Button>
</li>
))
}
</ul>
</div>
<div
className={ `flex justify-center md:justify-start w-full mt-8 ml-0 md:-ml-3` }>
<ul className={ `flex flex-col space-y-2 w-full` }>
{
entries.map((meta, key) => (
<li key={ key }>
<Button
as={ Link }
variant={ `light` }
color={ `default` }
fullWidth
isIconOnly={ isMobile }
className={ 'flex md:justify-start w-fit md:w-full' }
aria-label={ `navbar: ${ meta.display_name }` }
href={ meta.href }>
{ meta.icon }
<p className={ `font-bold hidden md:block` }>{ meta.display_name }</p>
</Button>
</li>
))
}
</ul>
</div>
<div className={ `mt-8 flex md:hidden justify-center w-full` }>
<p className={ `text-center sideways-lr font-bold text-2xl text-zinc-500 dark:text-zinc-400` }>
{ shownPath }
</p>
</div>
<div
className={ `absolute bottom-8 mb-8` }
>
<ThemeSwitcher />
</div>
</div>
</div>
</>
);
}
export default Navbar;
+42
View File
@@ -0,0 +1,42 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useTheme } from 'next-themes';
import { Switch } from '@nextui-org/react';
import { MdOutlineDarkMode, MdOutlineLightMode } from 'react-icons/md';
function ThemeSwitcher() {
const [ mounted, setMounted ] = useState(false);
const { setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<div className={ `` }>
<div className={ `flex flex-row items-center` }>
<Switch
color={ 'default' }
aria-label={ `theme switch` }
startContent={ <MdOutlineLightMode /> }
endContent={ <MdOutlineDarkMode /> }
onValueChange={ (isSelected) => {
if (isSelected) {
setTheme('light');
} else {
setTheme('dark');
}
} }
></Switch>
<p className={ `font-bold hidden md:block` }>{ `Toggle Theme` }</p>
</div>
</div>
);
}
export default ThemeSwitcher;
+13
View File
@@ -0,0 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'oss.lumither.com'
}
]
}
};
export default nextConfig;
+41
View File
@@ -0,0 +1,41 @@
{
"name": "blog",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@nextui-org/react": "^2.3.5",
"axios": "^1.6.8",
"framer-motion": "^11.2.10",
"lxgw-wenkai-webfont": "^1.7.0",
"next": "14.2.3",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.1.0",
"react-markdown": "^9.0.1",
"rehype-highlight": "^7.0.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
"remark-toc": "^9.0.0",
"sharp": "^0.33.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.2",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+60
View File
@@ -0,0 +1,60 @@
import { nextui } from '@nextui-org/react';
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
],
// theme: {
// extend: {}
// },
darkMode: 'class',
plugins: [
require('@tailwindcss/typography'),
nextui(
{
// defaultTheme: 'dark',
defaultExtendTheme: 'light',
themes: {
'light': {
layout: {},
colors: {}
},
'dark': {
layout: {},
colors: {}
}
}
}
),
plugin(function ({ addUtilities, addComponents, e, config }) {
addUtilities(
{
'.horizontal-tb': {
writingMode: 'horizontal-tb'
},
'.vertical-rl': {
writingMode: 'vertical-rl'
},
'.vertical-lr': {
writingMode: 'vertical-lr'
},
'.sideways-lr': {
writingMode: 'vertical-lr',
transform: 'rotate(180deg)'
},
'.sideways-rl': {
writingMode: 'sideways-rl',
transform: 'rotate(180deg)'
}
}
);
})
]
};
export default config;
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}