mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
feat(global) several major updates:
- delete page `tag`, `archive` - add page `friends` - implement page `about` *partially*
This commit is contained in:
18 files changed
+323
-113
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use std::{env, fs};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::{env, fs};
|
||||
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::StatusCode;
|
||||
@@ -9,7 +9,7 @@ use futures::{stream, StreamExt};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{PgPool, query};
|
||||
use sqlx::{query, PgPool};
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -42,6 +42,7 @@ pub async fn refresh(
|
||||
));
|
||||
}
|
||||
};
|
||||
let post_dir = Path::new(&data_repo_dir).join("posts");
|
||||
|
||||
if is_force_refresh {
|
||||
if let Err(e) = query("DROP TABLE IF EXISTS hash, meta, post, tag;")
|
||||
@@ -60,9 +61,9 @@ pub async fn refresh(
|
||||
}
|
||||
|
||||
// generate post idx
|
||||
let md_path = search_md(Path::new(&data_repo_dir));
|
||||
let post_path = search_md(&post_dir);
|
||||
|
||||
let post_list_with_hash: Vec<(Map<_, _>, Vec<_>)> = md_path
|
||||
let post_list_with_hash: Vec<(Map<_, _>, Vec<_>)> = post_path
|
||||
.into_iter()
|
||||
.filter_map(|post_path| match post::from_path(&post_path) {
|
||||
Ok(post) => match fs::read(&post_path) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
function Layout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={ 'flex p-7' }>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
import { fetchText } from '@/app/fetch';
|
||||
|
||||
const Me = async () => {
|
||||
return (
|
||||
<div className={ 'p-5' }>
|
||||
<MarkdownRenderer>
|
||||
{ await fetchText('https://raw.githubusercontent.com/lumither/blog-posts/master/about/me.md') }
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Me;
|
||||
@@ -1,13 +1,58 @@
|
||||
import { Card } from '@nextui-org/react';
|
||||
import React from 'react';
|
||||
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
|
||||
import { CardBody } from '@nextui-org/card';
|
||||
import Link from 'next/link';
|
||||
|
||||
function About() {
|
||||
const About = () => {
|
||||
return (
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
<div className={ `flex flex-col w-full justify-center align-middle h-dvh` }>
|
||||
<div className={ 'md:relative md:h-[130px] mb-4 md:mb-0' }>
|
||||
<div className={ 'md:relative md:left-[20%] flex flex-col w-fit' }>
|
||||
<p className={ 'text-pretty text-3xl' }>{ '讷,远道而来的' }<span
|
||||
className={ 'break-keep underline decoration-sky-400 dark:decoration-sky-700 decoration-4 underline-offset-0' }>旅人</span>
|
||||
</p>
|
||||
<p className={ 'text-pretty md:text-2xl mt-1 md:ml-4' }>{ '愿意坐下来听个故事吗...' }</p>
|
||||
<p className={ 'hidden md:block text-lg md:ml-8' }>{ '一个关于一位' }<span
|
||||
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ '追寻' }<span
|
||||
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ ',探索' }<span
|
||||
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ '的故事' }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ `flex justify-center w-full h-[60%] md:h-[40%]` }>
|
||||
<div
|
||||
className={ `flex flex-col md:flex-row w-full md:w-[85%] lg:w-[90%] xl:w-[85%] p-4 gap-x-7 gap-y-5` }>
|
||||
<Card
|
||||
isHoverable
|
||||
className={ 'w-full h-[45%] md:h-full' }
|
||||
as={ Link }
|
||||
href={ '/about/me' }
|
||||
isPressable
|
||||
>
|
||||
<CardBody>
|
||||
<div className={ 'h-full flex flex-col justify-center' }>
|
||||
<p className={ 'text-center text-3xl h-fit' }>关于我</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card
|
||||
isHoverable
|
||||
className={ 'w-full h-[45%] md:h-full' }
|
||||
as={ Link }
|
||||
href={ '/about/website' }
|
||||
isPressable
|
||||
>
|
||||
<CardBody>
|
||||
<div className={ 'h-full flex flex-col justify-center' }>
|
||||
<p className={ 'text-center text-3xl h-fit' }>关于这个网站</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<p className={ 'text-center' }>*目前还没想到如何设计这一页,先摸鱼会(笑)</p>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
import { fetchText } from '@/app/fetch';
|
||||
|
||||
const Website = async () => {
|
||||
return (
|
||||
<div className={ 'p-5' }>
|
||||
<MarkdownRenderer>
|
||||
{ await fetchText('https://raw.githubusercontent.com/lumither/blog-posts/master/about/website.md') }
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Website;
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
|
||||
|
||||
const Archive = () => {
|
||||
return (
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Archive;
|
||||
@@ -6,9 +6,10 @@ function Layout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
// <div className={ 'flex flex-auto p-7 pl-2 md:pl-7 items-stretch justify-center' }>
|
||||
<div className={ 'flex flex-auto p-7 pl-2 lg:pl-7 w-full justify-center' }>
|
||||
{ children }
|
||||
<main className={ 'w-full' }>
|
||||
{ children }
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { IoArrowBackOutline } from 'react-icons/io5';
|
||||
|
||||
const ButtonGoBack = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Button
|
||||
startContent={ <IoArrowBackOutline /> }
|
||||
variant={ 'light' }
|
||||
onClick={ () => router.back() }
|
||||
>
|
||||
{ 'Back' }
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
export default ButtonGoBack;
|
||||
@@ -1,16 +1,9 @@
|
||||
import React from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import { Card, CardBody } from '@nextui-org/card';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkToc from 'remark-toc';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import Headers from '@/app/blog/post/[id]/Headers';
|
||||
import Footer from '@/app/blog/post/[id]/Footer';
|
||||
import { fetchNocturneApi } from '@/app/blog/api';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
@@ -31,55 +24,15 @@ async function PostCard(props: Props) {
|
||||
|
||||
{/* card body */ }
|
||||
<CardBody>
|
||||
<Markdown
|
||||
// className={ `p-5 max-w-full text-justify text-pretty prose prose-neutral prose-sm dark:prose-invert md:prose-md lg:prose-lg` }
|
||||
className={ `p-5 max-w-full text-justify text-pretty prose prose-sm prose-neutral dark:prose-invert md:prose-md lg:prose-lg
|
||||
prose-code:overflow-x-scroll
|
||||
` }
|
||||
remarkPlugins={ [ remarkGfm, remarkToc, remarkFrontmatter ] }
|
||||
rehypePlugins={ [ rehypeRaw, rehypeSanitize ] }
|
||||
components={ {
|
||||
pre(props) {
|
||||
const { node, className, children, ...rest } = props;
|
||||
if ((children as any)['type'] === 'code') {
|
||||
const match = /language-(\w+)/.exec((children as any)['props']['className'] || '');
|
||||
let lang = match ? match[1] : 'text';
|
||||
return (
|
||||
<pre className={ `not-prose` }>
|
||||
<SyntaxHighlighter
|
||||
// @ts-ignore
|
||||
style={ oneDark }
|
||||
language={ lang }
|
||||
showLineNumbers
|
||||
// wrapLongLines
|
||||
// wrapLines
|
||||
classNames={ className }
|
||||
PreTag="div"
|
||||
{ ...rest }
|
||||
>
|
||||
{
|
||||
String((children as any)['props']['children']).replace(/\n$/, '')
|
||||
}
|
||||
</SyntaxHighlighter>
|
||||
</pre>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<pre className={ `not-prose` }>
|
||||
<code className={ className } { ...props }>
|
||||
{ children }
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
} }
|
||||
>{ ret['content'] }</Markdown>
|
||||
<div className={ 'p-5' }>
|
||||
<MarkdownRenderer>
|
||||
{ ret['content'] }
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
|
||||
{/* card footer */ }
|
||||
|
||||
<Footer post={ ret } />
|
||||
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PostCard from '@/app/blog/post/[id]/PostCard';
|
||||
import { MotionDiv } from '@/app/public/MotionDiv';
|
||||
import ButtonGoBack from '@/app/blog/post/[id]/ButtonGoBack';
|
||||
|
||||
function BlogReader({ params }: { params: { id: string } }) {
|
||||
|
||||
@@ -14,6 +15,9 @@ function BlogReader({ params }: { params: { id: string } }) {
|
||||
<div className={ `max-w-full` }>
|
||||
<PostCard id={ params.id } />
|
||||
</div>
|
||||
<div className={ 'mt-3' }>
|
||||
<ButtonGoBack />
|
||||
</div>
|
||||
</MotionDiv>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export async function fetchText(url: string, ttl: number = 600) {
|
||||
const res = await fetch(url, {
|
||||
next: {
|
||||
revalidate: ttl
|
||||
}
|
||||
});
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
export async function fetchJson(url: string, ttl: number = 600) {
|
||||
const res = await fetch(url, {
|
||||
next: {
|
||||
revalidate: ttl
|
||||
}
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
function Layout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={ 'p-7' }>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { fetchJson } from '@/app/fetch';
|
||||
import { Card, CardBody } from '@nextui-org/react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function Friends() {
|
||||
|
||||
let list = await fetchJson('https://raw.githubusercontent.com/Lumither/friends/master/friends.json');
|
||||
|
||||
const listLength = list.length;
|
||||
const listMaxIdx = listLength - 1;
|
||||
const listIsOdd = listLength % 2 !== 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={ `min-h-dvh pb-10` }>
|
||||
<div className={ 'p-7 pt-[10%] md:pt-[20%]' }>
|
||||
<div className={ 'flex flex-col md:flex-row gap-x-3 justify-center' }>
|
||||
<p className={ 'text-3xl text-center text-balance' }>海内存知己</p>
|
||||
<p className={ 'text-3xl text-center text-balance' }>天涯若比邻</p>
|
||||
</div>
|
||||
<p className={ 'text-center text-balance text-zinc-500 dark:text-zinc-400' }>{ 'Though far apart, kin at heart' }</p>
|
||||
</div>
|
||||
<div className={ 'flex w-full h-full justify-center pt-3' }>
|
||||
<ul className={ 'grid grid-cols-2 gap-x-6 gap-y-3 md:max-w-[90%] xl:max-w-[85%] w-full' }>
|
||||
{
|
||||
list.map((friend: any, key: number) => (
|
||||
<li
|
||||
key={ key }
|
||||
className={ (key === listMaxIdx && listIsOdd) ? 'col-span-2 flex justify-center' : 'col-span-2 md:col-span-1' }
|
||||
>
|
||||
<Card
|
||||
isHoverable
|
||||
isPressable
|
||||
shadow={ 'none' }
|
||||
as={ Link }
|
||||
href={ friend['url'] }
|
||||
className={ 'dark:bg-[#282830]' }
|
||||
>
|
||||
<CardBody className={ 'pb-0' }>
|
||||
<div className={ 'flex flex-row' }>
|
||||
<div className={ 'flex p-2' }>
|
||||
<div
|
||||
className={ 'h-[80px] w-[80px] relative rounded-2xl' }>
|
||||
<Image
|
||||
alt="avatar"
|
||||
className="object-cover rounded-2xl z-10"
|
||||
fill
|
||||
unoptimized
|
||||
src={ friend['avatar'] }
|
||||
/>
|
||||
<Image
|
||||
alt="avatar"
|
||||
className="relative object-cover rounded-2xl scale-110 blur-3xl"
|
||||
fill
|
||||
unoptimized
|
||||
src={ friend['avatar'] }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ 'p-2 flex-col max-w-full' }>
|
||||
<p className={ 'text-xl' }>{ friend['title'] }</p>
|
||||
{ (friend['name'] === undefined || friend['name'].isNull) ??
|
||||
<p className={ 'text-zinc-600 dark:text-zinc-400' }>{ `@ ${ friend['name'] }` }</p> }
|
||||
{ (friend['desc'] === undefined || friend['desc'].isNull) ??
|
||||
<p className={ 'text-sm text-pretty text-zinc-600 dark:text-zinc-300 mt-1' }>{ friend['desc'] }</p> }
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
<div className={ 'h-1 bg-blue-800 dark:bg-blue-300' } />
|
||||
</Card>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p className={ 'text-center' }>想要成为好伙伴嘛,向<span
|
||||
className={ 'text-blue-800 dark:text-blue-500' }><Link
|
||||
href={ 'https://github.com/Lumither/friends/' }>这里</Link></span>发起PR吧
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,8 @@ import React from 'react';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
</div>
|
||||
</main>
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkToc from 'remark-toc';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
const MarkdownRenderer = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className={ 'w-full' }>
|
||||
<Markdown
|
||||
className={ `max-w-full text-justify text-pretty prose prose-sm prose-neutral dark:prose-invert
|
||||
md:prose-md lg:prose-lg prose-code:overflow-x-scroll`
|
||||
}
|
||||
remarkPlugins={ [ remarkGfm, remarkToc, remarkFrontmatter ] }
|
||||
rehypePlugins={ [ rehypeRaw, rehypeSanitize ] }
|
||||
components={ {
|
||||
pre(props) {
|
||||
const { node, className, children, ...rest } = props;
|
||||
if ((children as any)['type'] === 'code') {
|
||||
const match = /language-(\w+)/.exec((children as any)['props']['className'] || '');
|
||||
let lang = match ? match[1] : 'text';
|
||||
return (
|
||||
<pre className={ `not-prose` }>
|
||||
<SyntaxHighlighter
|
||||
// @ts-ignore
|
||||
style={ oneDark }
|
||||
language={ lang }
|
||||
showLineNumbers
|
||||
// wrapLongLines
|
||||
// wrapLines
|
||||
classNames={ className }
|
||||
PreTag="div"
|
||||
{ ...rest }
|
||||
>
|
||||
{
|
||||
String((children as any)['props']['children']).replace(/\n$/, '')
|
||||
}
|
||||
</SyntaxHighlighter>
|
||||
</pre>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<pre className={ `not-prose` }>
|
||||
<code className={ className } { ...props }>
|
||||
{ children }
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
} }
|
||||
>
|
||||
{
|
||||
children as any
|
||||
}
|
||||
</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MarkdownRenderer;
|
||||
Whitespace-only changes.
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
|
||||
|
||||
const Tags = () => {
|
||||
return (
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -3,16 +3,7 @@
|
||||
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 { IoHome, IoInformationCircle, IoLink, IoLogoGithub, IoMail, IoPencil, IoSearch } from 'react-icons/io5';
|
||||
import Link from 'next/link';
|
||||
import ThemeSwitcher from '@/components/ThemeSwitcher';
|
||||
import { usePathname } from 'next/navigation';
|
||||
@@ -34,14 +25,9 @@ const entries: { display_name: string; href: string; icon: React.ReactNode }[] =
|
||||
href: '/blog'
|
||||
},
|
||||
{
|
||||
icon: <IoPricetags size={ `20px` }></IoPricetags>,
|
||||
display_name: 'Tags',
|
||||
href: '/tags'
|
||||
},
|
||||
{
|
||||
icon: <IoFileTrayFull size={ `20px` }></IoFileTrayFull>,
|
||||
display_name: 'Archive',
|
||||
href: '/archive'
|
||||
icon: <IoLink size={ `20px` }></IoLink>,
|
||||
display_name: 'Friends',
|
||||
href: '/friends'
|
||||
},
|
||||
{
|
||||
icon: <IoSearch size={ `20px` }></IoSearch>,
|
||||
@@ -116,7 +102,7 @@ function Navbar() {
|
||||
<div className={ `hidden lg:block` }>
|
||||
<div className={ `flex justify-center items-center my-7` }>
|
||||
<NextImage
|
||||
src={ 'https://oss.lumither.com/blog/pictures/compr/1717153492846703409_avatar.webp' }
|
||||
src={ 'https://oss.lumither.com/blog/pictures/avatar.webp' }
|
||||
alt={ 'avatar' }
|
||||
width={ 200 }
|
||||
height={ 200 }
|
||||
|
||||
Reference in new issue
Block a user