mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 18:36:21 +00:00
Compare commits
1
Commits
release
...
8938c96409
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8938c96409 |
No files matched your search
@@ -1,36 +1,16 @@
|
||||
# `Nocturne: personal website & CMS`
|
||||
# `Nocturne: Personal Website`
|
||||
|
||||
This is the code repo of my personal website, and [here](https://github.com/Lumither/blog-posts/) is the link of the
|
||||
content repo.
|
||||
This is the code repo of my [personal website](https://lumither.com),
|
||||
and [here](https://github.com/Lumither/blog-posts/) is the link to the
|
||||
content (blog posts etc.) repo.
|
||||
|
||||
Powered by Next.js and Rust, built with passion and love
|
||||
|
||||
<p align="center">
|
||||
<img src="https://avatars.githubusercontent.com/u/46409277?v=4" alt="GitHub Avatar"/>
|
||||
</p>
|
||||
> [!NOTE]
|
||||
> This system is not production ready, and contains bugs, (security risk potentially).
|
||||
> But have fun on it =)
|
||||
|
||||
*will add more here in the future
|
||||
|
||||
## Nav:
|
||||
|
||||
<todo>
|
||||
|
||||
## Todos:
|
||||
|
||||
### Backend
|
||||
|
||||
- [ ] Add: log system
|
||||
- [ ] Add: search api
|
||||
- [ ] Add: tag filter (api)
|
||||
- [ ] Abandon: UUID mechanism
|
||||
- [ ] Fix: db connection issue
|
||||
|
||||
### Frontend
|
||||
|
||||
- [ ] Page: Home
|
||||
- [ ] Page: About
|
||||
- [ ] Page: Tags
|
||||
- [ ] Page: Archive
|
||||
- [ ] Page: Search
|
||||
- [ ] Add: Right sidebar
|
||||
# Todos
|
||||
|
||||
- [ ] the todo list lol
|
||||
- [ ] and much more...
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fs::remove_dir_all;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -89,24 +90,22 @@ pub fn task(db_connection: &Pool<Postgres>) {
|
||||
for update in updates {
|
||||
if [Delta::Deleted, Delta::Untracked].contains(&update.status) {
|
||||
if let Some(path) = &update.old_path {
|
||||
if drop_index(
|
||||
match drop_index(
|
||||
&get_md_file_basename(
|
||||
path.file_name().unwrap().to_str().unwrap(),
|
||||
),
|
||||
db_connection,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
info!(
|
||||
) {
|
||||
Ok(_) => info!(
|
||||
"blog content git({}) file `{}` is detected as deleted/untracked, index dropped",
|
||||
&git_url,
|
||||
&path.display()
|
||||
);
|
||||
} else {
|
||||
error!(
|
||||
"blog content git({}) file `{}` is detected as deleted/untracked, failed to drop index",
|
||||
),
|
||||
Err(e) => error!(
|
||||
"blog content git({}) file `{}` is detected as deleted/untracked, failed to drop index: {}",
|
||||
&git_url,
|
||||
&path.display()
|
||||
&path.display(),
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -114,7 +113,8 @@ pub fn task(db_connection: &Pool<Postgres>) {
|
||||
if path.components().any(|comp| comp.as_os_str() == "posts")
|
||||
&& path.extension().is_some_and(|ext| ext == "md")
|
||||
{
|
||||
update_md.push(path.to_path_buf());
|
||||
// update_md.push(path.to_path_buf());
|
||||
update_md.push(dbg!(path.to_path_buf()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn search_md(entry: PathBuf) -> Vec<PathBuf> {
|
||||
pub fn index_files(files: &[PathBuf], db_conn: &Pool<Postgres>) {
|
||||
let rt = Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let files_clone = files.to_owned();
|
||||
let files_clone = dbg!(files).to_owned();
|
||||
let handlers = files_clone
|
||||
.into_iter()
|
||||
.map(|file: PathBuf| {
|
||||
|
||||
@@ -7,7 +7,8 @@ pub mod check_update;
|
||||
pub fn get_tasks(db_connection: Pool<Postgres>) -> Vec<Box<dyn Task>> {
|
||||
let db_conn_clone = db_connection.clone();
|
||||
vec![
|
||||
BasicTask::new(move || check_update::task(&db_conn_clone), "0/15 * * * * *")
|
||||
// BasicTask::new(move || check_update::task(&db_conn_clone), "0/15 * * * * *")
|
||||
BasicTask::new(move || check_update::task(&db_conn_clone), "* * * * * *")
|
||||
.unwrap()
|
||||
.to_task(),
|
||||
]
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `About - ${ SITE_CONFIG.name }`
|
||||
};
|
||||
|
||||
function Layout({
|
||||
children
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
||||
import { fetchText } from '@/app/fetch';
|
||||
|
||||
const Me = async () => {
|
||||
File renamed without changes.
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import MarkdownRenderer from '@/app/public/MarkdownRenderer';
|
||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
||||
import { fetchText } from '@/app/fetch';
|
||||
|
||||
const Website = async () => {
|
||||
File renamed without changes.
File renamed without changes.
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import { Pagination as NextUIPagination } from '@nextui-org/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
||||
|
||||
const Pagination = ({ total }: {
|
||||
total: number,
|
||||
@@ -1,11 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import React from 'react';
|
||||
import { fetchNocturneApi } from '@/app/blog/api';
|
||||
import { MotionDiv } from '@/app/public/MotionDiv';
|
||||
import BlogPostCard from '@/app/blog/BlogPostCard';
|
||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
||||
import BlogPostCard from '@/app/(pages)/blog/BlogPostCard';
|
||||
import { Card, CardBody } from '@nextui-org/card';
|
||||
|
||||
import * as motion from 'motion/react-client';
|
||||
|
||||
const PostList = async ({ page }: { page: number }) => {
|
||||
try {
|
||||
const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`);
|
||||
@@ -16,13 +17,13 @@ const PostList = async ({ page }: { page: number }) => {
|
||||
<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
|
||||
<motion.div
|
||||
initial={ { y: 20, opacity: 0 } }
|
||||
animate={ { y: 0, opacity: 1 } }
|
||||
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
|
||||
>
|
||||
<BlogPostCard post={ post } />
|
||||
</MotionDiv>
|
||||
</motion.div>
|
||||
</li>)
|
||||
) }
|
||||
</ul>
|
||||
File renamed without changes.
@@ -1,4 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Blog - ${ SITE_CONFIG.name }`
|
||||
};
|
||||
|
||||
function Layout({
|
||||
children
|
||||
@@ -1,6 +1,6 @@
|
||||
import PostList from '@/app/blog/PostList';
|
||||
import Pagination from '@/app/blog/Pagination';
|
||||
import { fetchNocturneApi } from '@/app/blog/api';
|
||||
import PostList from '@/app/(pages)/blog/PostList';
|
||||
import Pagination from '@/app/(pages)/blog/Pagination';
|
||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
||||
|
||||
async function Blog({
|
||||
searchParams
|
||||
File renamed without changes.
+21
-13
@@ -1,24 +1,32 @@
|
||||
import React from 'react';
|
||||
import MinifyPostCard from '@/app/blog/post/[id]/MinifyPostCard';
|
||||
import MinifyPostCard from '@/app/(pages)/blog/post/[id]/MinifyPostCard';
|
||||
import Link from 'next/link';
|
||||
import { FaCreativeCommons } from 'react-icons/fa6';
|
||||
|
||||
type Props = {
|
||||
post: any
|
||||
// post: any
|
||||
post_id: string;
|
||||
adjacent_posts: any
|
||||
perm_link: string | undefined;
|
||||
author: string | undefined;
|
||||
author_link: string | undefined;
|
||||
first_update: string;
|
||||
last_update: string | undefined;
|
||||
cc: string | undefined;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const Footer = (props: Props) => {
|
||||
const { post } = props;
|
||||
const { adjacent_posts, perm_link, post_id, first_update, last_update, title } = props;
|
||||
|
||||
const adjacent_posts = post['adj'];
|
||||
const prev_post = adjacent_posts['prev'];
|
||||
const next_post = adjacent_posts['next'];
|
||||
const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`;
|
||||
const author = post['meta']['author'] ?? 'Tao';
|
||||
const author_link = post['meta']['author_link'] ?? '/about';
|
||||
const post_date = new Date(post['first_update']);
|
||||
const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined;
|
||||
const cc = post['meta']['cc'] ?? 'by-nc-sa';
|
||||
const prev_post = adjacent_posts.prev;
|
||||
const next_post = adjacent_posts.next;
|
||||
const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`;
|
||||
const author = props.author ?? 'Tao';
|
||||
const author_link = props.author_link ?? '/about/me';
|
||||
const post_date = new Date(first_update);
|
||||
const update_date: Date | undefined = last_update ? new Date(last_update) : undefined;
|
||||
const cc = props.cc ?? 'by-nc-sa';
|
||||
|
||||
|
||||
return (
|
||||
@@ -35,7 +43,7 @@ const Footer = (props: Props) => {
|
||||
|
||||
<div className={ 'flex flex-col space-y-4 relative' }>
|
||||
<div className={ 'z-[2]' }>
|
||||
<p className={ 'text-xl font-bold' }>{ post['meta']['title'] }</p>
|
||||
<p className={ 'text-xl font-bold' }>{ title }</p>
|
||||
<Link href={ link }>
|
||||
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
|
||||
</Link>
|
||||
+23
-23
@@ -3,57 +3,57 @@ import Image from 'next/image';
|
||||
import { Chip } from '@nextui-org/react';
|
||||
import { MdCalendarMonth } from 'react-icons/md';
|
||||
|
||||
// type Props = {
|
||||
// post: any
|
||||
// };
|
||||
|
||||
type Props = {
|
||||
post: any
|
||||
};
|
||||
title: string;
|
||||
subtitle: string;
|
||||
summary: string | undefined;
|
||||
category: string;
|
||||
tags: string[];
|
||||
first_update: string;
|
||||
header_img: string | undefined;
|
||||
}
|
||||
|
||||
|
||||
const Headers = (props: Props) => {
|
||||
let post = props.post;
|
||||
let title = post['title'];
|
||||
let sub_title = post['sub_title'];
|
||||
// let summary = (post as any)['summary'];
|
||||
let category = post['category'];
|
||||
let tags = post['tags'];
|
||||
// let last_update = new Date((post as any)['last_update']);
|
||||
let first_update = new Date(post['first_update']);
|
||||
// let post_id = (post as any)['post_id'];
|
||||
let header_img = post['meta']['header_img'] as string | null;
|
||||
const Headers = (post: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{ header_img && <div className={ `w-full overflow h-96 relative` }>
|
||||
{ post.header_img && <div className={ `w-full overflow h-96 relative` }>
|
||||
<Image
|
||||
alt={ 'cover picture' }
|
||||
src={ header_img }
|
||||
src={ post.header_img }
|
||||
className={ `w-full object-cover rounded-b-none z-[5]` }
|
||||
fill
|
||||
></Image>
|
||||
<Image
|
||||
alt={ 'cover picture' }
|
||||
src={ header_img }
|
||||
src={ post.header_img }
|
||||
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
|
||||
fill
|
||||
></Image>
|
||||
</div> }
|
||||
|
||||
<div
|
||||
className={ header_img ?
|
||||
className={ post.header_img ?
|
||||
'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30'
|
||||
: 'mx-6 mt-6'
|
||||
}>
|
||||
<div className={ header_img ? `p-4` : 'p-4 pt-2' }>
|
||||
<div className={ post.header_img ? `p-4` : 'p-4 pt-2' }>
|
||||
<Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
|
||||
variant={ `solid` }>{ category }</Chip>
|
||||
<p className={ `text-4xl font-bold` }> { title }</p>
|
||||
<p className={ `text-xl` }> { sub_title }</p>
|
||||
variant={ `solid` }>{ post.category }</Chip>
|
||||
<p className={ `text-4xl font-bold` }> { post.title }</p>
|
||||
<p className={ `text-xl` }> { post.subtitle }</p>
|
||||
</div>
|
||||
|
||||
<div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }>
|
||||
<div>
|
||||
<ul className={ `flex flex-row flex-wrap` }>
|
||||
{
|
||||
tags.map((tag: string, key: any) => (
|
||||
post.tags.map((tag: string, key: any) => (
|
||||
<li key={ key } className={ `mx-1 mb-1` }>
|
||||
<Chip>{ tag }</Chip>
|
||||
</li>
|
||||
@@ -64,7 +64,7 @@ const Headers = (props: Props) => {
|
||||
<div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }>
|
||||
<MdCalendarMonth />
|
||||
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
|
||||
{ first_update.toLocaleString('default', {
|
||||
{ (new Date(post.first_update)).toLocaleString('default', {
|
||||
month: `long`,
|
||||
day: `numeric`,
|
||||
year: `numeric`
|
||||
+4
-4
@@ -43,10 +43,10 @@ const MinifyPostCard = (props: Props) => {
|
||||
}
|
||||
{ !img_url &&
|
||||
<div className={ 'h-full grid p-6 place-content-center' }>
|
||||
<div className={ 'flex flex-row items-center text-center max-h-fit' }>
|
||||
<p className={ 'text-3xl font-bold' }>{ post['title'] }</p>
|
||||
<div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div>
|
||||
<p className={ 'text-xl' }>{ post['sub_title'] }</p>
|
||||
<div className={ 'flex flex-col max-h-fit text-left' }>
|
||||
<p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p>
|
||||
<div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div>
|
||||
<p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Card, CardBody } from '@nextui-org/card';
|
||||
import Headers from '@/app/(pages)/blog/post/[id]/Headers';
|
||||
import Footer from '@/app/(pages)/blog/post/[id]/Footer';
|
||||
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
|
||||
|
||||
type Props = {
|
||||
post: any
|
||||
}
|
||||
|
||||
async function PostCard(props: Props) {
|
||||
let post = props.post;
|
||||
return (
|
||||
<div>
|
||||
<Card className={ `max-w-full relative` }>
|
||||
|
||||
{/* card headers */ }
|
||||
<Headers title={ post.title }
|
||||
subtitle={ post.sub_title }
|
||||
summary={ post.summary }
|
||||
category={ post.category }
|
||||
tags={ post.tags }
|
||||
first_update={ post.first_update }
|
||||
header_img={ post.meta.header_img }
|
||||
/>
|
||||
|
||||
{/* card body */ }
|
||||
<CardBody>
|
||||
<div className={ 'p-5' }>
|
||||
<MarkdownRenderer>
|
||||
{ post.content }
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
|
||||
{/* card footer */ }
|
||||
<Footer
|
||||
adjacent_posts={ post.adj }
|
||||
perm_link={ post.meta.link }
|
||||
author={ post.meta.author }
|
||||
author_link={ post.meta.author_link }
|
||||
first_update={ post.first_update }
|
||||
last_update={ post.last_update }
|
||||
cc={ post.meta.cc }
|
||||
post_id={ post.meta.id }
|
||||
title={ post.title }
|
||||
/>
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PostCard;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { cache } from 'react';
|
||||
import PostCard from '@/app/(pages)/blog/post/[id]/PostCard';
|
||||
import ButtonGoBack from '@/app/(pages)/blog/post/[id]/ButtonGoBack';
|
||||
import * as motion from 'motion/react-client';
|
||||
import { fetchNocturneApi } from '@/app/(pages)/blog/api';
|
||||
import { Metadata } from 'next';
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
const fetchPost = cache(async (id: string) => {
|
||||
const res = await fetchNocturneApi(`/blog/get_post/${ id }`);
|
||||
// throw Error('Not Found');
|
||||
return await res.json();
|
||||
});
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: { params: Promise<{ id: string }> })
|
||||
: Promise<Metadata> {
|
||||
try {
|
||||
const title = (await fetchPost((await params).id)).title as string;
|
||||
return {
|
||||
title: `${ title } - ${ SITE_CONFIG.name }`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
title: 'Unexpected Error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function BlogReader({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
try {
|
||||
let post = await fetchPost((await params).id);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={ `max-w-full` }
|
||||
initial={ { y: 20, opacity: 0 } }
|
||||
animate={ { y: 0, opacity: 1 } }
|
||||
transition={ { ease: 'easeInOut', duration: 0.5 } }
|
||||
>
|
||||
<div className={ `max-w-full` }>
|
||||
<PostCard post={ post } />
|
||||
</div>
|
||||
<div className={ 'mt-3' }>
|
||||
<ButtonGoBack />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
} catch (e: any) {
|
||||
return (
|
||||
<div className={ `flex h-full w-full justify-center items-center` }>
|
||||
<div className={ `flex-row` }>
|
||||
<p className={ `text-xl font-bold` }>
|
||||
{ 'Unexpected Error:' }
|
||||
</p>
|
||||
<p className={ `font-bold` }>
|
||||
{ e.message }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BlogReader;
|
||||
@@ -1,4 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Friends - ${ SITE_CONFIG.name }`
|
||||
};
|
||||
|
||||
function Layout({
|
||||
children
|
||||
File renamed without changes.
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
import * as motion from 'motion/react-client';
|
||||
|
||||
function Layout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
initial={ { opacity: 0 } }
|
||||
animate={ { opacity: 1 } }
|
||||
transition={ { ease: 'easeInOut', duration: 0.3, delay: 0.5 } }
|
||||
className={ `justify-center flex flex-row w-full max-w-full` }>
|
||||
<Navbar />
|
||||
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
|
||||
{ children }
|
||||
<Footer />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Activity } from '@/src/HomePageActivities/activity';
|
||||
import { getGhActivities } from '@/src/HomePageActivities/ghActivity';
|
||||
import * as motion from 'motion/react-client';
|
||||
|
||||
const Activities = () => {
|
||||
|
||||
const [ activities, setActivities ] = useState<Activity[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getGhActivities().then(data => setActivities(data));
|
||||
}, []);
|
||||
|
||||
if (!activities) {
|
||||
return (
|
||||
<div>
|
||||
<p>loading</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<ul className={ 'flex flex-col gap-y-3 items-end' }>
|
||||
{ activities.map((activity, key) => (
|
||||
<motion.div
|
||||
key={ key }
|
||||
initial={ { opacity: 0, y: 20 } }
|
||||
animate={ { opacity: 1, y: 0 } }
|
||||
transition={ { ease: 'easeInOut', duration: 0.2 } }
|
||||
>
|
||||
<li className={ 'backdrop-blur bg-white/75 dark:bg-black/45 rounded-xl w-fit' }>
|
||||
{ activity.toElement() }
|
||||
</li>
|
||||
</motion.div>
|
||||
)) }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default Activities;
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, CardBody } from '@nextui-org/card';
|
||||
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
|
||||
}
|
||||
|
||||
async function PostCard(props: Props) {
|
||||
|
||||
|
||||
try {
|
||||
const res = await fetchNocturneApi(`/blog/get_post/${ props.id }`);
|
||||
const ret: any = await res.json();
|
||||
return (
|
||||
<div>
|
||||
<Card className={ `max-w-full relative` }>
|
||||
|
||||
{/* card headers */ }
|
||||
<Headers post={ ret } />
|
||||
|
||||
{/* card body */ }
|
||||
<CardBody>
|
||||
<div className={ 'p-5' }>
|
||||
<MarkdownRenderer>
|
||||
{ ret['content'] }
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
|
||||
{/* card footer */ }
|
||||
<Footer post={ ret } />
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
} catch (e: any) {
|
||||
return <div>
|
||||
<Card className={ `w-full` }>
|
||||
<CardBody>
|
||||
Fatal: { e.toString() }
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PostCard;
|
||||
@@ -1,29 +0,0 @@
|
||||
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';
|
||||
|
||||
async function BlogReader({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
|
||||
return (
|
||||
<MotionDiv
|
||||
className={ `max-w-full` }
|
||||
initial={ { y: 20, opacity: 0 } }
|
||||
animate={ { y: 0, opacity: 1 } }
|
||||
transition={ { ease: 'easeInOut', duration: 0.5 } }
|
||||
>
|
||||
<div className={ `max-w-full` }>
|
||||
<PostCard id={ (await params).id } />
|
||||
</div>
|
||||
<div className={ 'mt-3' }>
|
||||
<ButtonGoBack />
|
||||
</div>
|
||||
</MotionDiv>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogReader;
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
IoFlask,
|
||||
IoHome,
|
||||
IoInformationCircle,
|
||||
IoLink,
|
||||
IoLogoGithub,
|
||||
IoMail,
|
||||
IoPencil,
|
||||
IoSearch
|
||||
} from 'react-icons/io5';
|
||||
|
||||
export 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: <IoLink size={ `20px` }></IoLink>,
|
||||
display_name: 'Friends',
|
||||
href: '/friends'
|
||||
},
|
||||
{
|
||||
icon: <IoSearch size={ `20px` }></IoSearch>,
|
||||
display_name: 'Search',
|
||||
href: '/search'
|
||||
}, {
|
||||
icon: <IoFlask size={ '20px' } />,
|
||||
display_name: 'Lab',
|
||||
href: 'https://lab.lmt.moe'
|
||||
}
|
||||
];
|
||||
|
||||
export 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` } />
|
||||
}
|
||||
];
|
||||
+6
-11
@@ -2,13 +2,14 @@ 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';
|
||||
|
||||
import { SITE_CONFIG } from '@/src/constants';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Lumitherの酒馆',
|
||||
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...'
|
||||
openGraph: {
|
||||
siteName: SITE_CONFIG.name
|
||||
},
|
||||
title: SITE_CONFIG.name,
|
||||
description: SITE_CONFIG.desc
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -20,13 +21,7 @@ export default function RootLayout({
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }>
|
||||
<NextThemesProvider attribute="class" defaultTheme="system">
|
||||
<div className={ `justify-center flex flex-row w-full max-w-full` }>
|
||||
<Navbar />
|
||||
<div className={ `flex flex-col flex-1 max-w-[1024px] min-w-0` }>
|
||||
{ children }
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</NextThemesProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+44
-3
@@ -1,10 +1,51 @@
|
||||
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { entries } from '@/app/entries';
|
||||
import Link from 'next/link';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
export default function Home() {
|
||||
|
||||
const isMobile = useScreenSizeTrigger('lg');
|
||||
|
||||
const [ test, setTest, removeTest ] = useLocalStorage('test', 'test_value');
|
||||
console.log(test);
|
||||
// setTest('test value 2');
|
||||
// console.log(test);
|
||||
// removeTest();
|
||||
// console.log(test);
|
||||
|
||||
return (
|
||||
<div className={ `h-dvh` }>
|
||||
<ConstructionPlaceHolder />
|
||||
<div className={ `h-dvh relative` }>
|
||||
<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 lg:justify-start w-fit lg:w-full' }
|
||||
aria-label={ `navbar: ${ meta.display_name }` }
|
||||
href={ meta.href }
|
||||
>
|
||||
{ meta.icon }
|
||||
<p className={ `font-bold hidden lg:block` }>{ meta.display_name }</p>
|
||||
</Button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div
|
||||
className={ 'absolute bottom-0 w-full h-60 bg-yellow-500 p-12' }
|
||||
>
|
||||
<div className={ 'bg-blue-500 h-full w-full rounded-3xl backdrop-blur' }></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const MotionDiv = motion.div;
|
||||
@@ -3,65 +3,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
IoFlask,
|
||||
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';
|
||||
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
||||
import { connections, entries } from '@/app/entries';
|
||||
|
||||
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: <IoLink size={ `20px` }></IoLink>,
|
||||
display_name: 'Friends',
|
||||
href: '/friends'
|
||||
},
|
||||
{
|
||||
icon: <IoSearch size={ `20px` }></IoSearch>,
|
||||
display_name: 'Search',
|
||||
href: '/search'
|
||||
}, {
|
||||
icon: <IoFlask size={ '20px' } />,
|
||||
display_name: 'Lab',
|
||||
href: 'https://lab.lmt.moe'
|
||||
}
|
||||
];
|
||||
|
||||
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() {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
|
||||
import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md';
|
||||
import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
|
||||
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
|
||||
|
||||
const switcherElements = new Map(
|
||||
[
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.4.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"framer-motion": "^11.3.24",
|
||||
"lxgw-wenkai-webfont": "^1.7.0",
|
||||
"motion": "^12.3.1",
|
||||
"next": "^15.0.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -20,6 +20,7 @@
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use": "^17.6.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export abstract class Activity {
|
||||
protected constructor(
|
||||
readonly time: Date
|
||||
) {
|
||||
}
|
||||
|
||||
abstract toElement(): React.ReactElement;
|
||||
|
||||
}
|
||||
|
||||
export abstract class GhActivity extends Activity {
|
||||
protected constructor(time: Date) {
|
||||
super(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const GH_USER_NAME = 'Lumither';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { GhActivity } from '@/src/HomePageActivities/activity';
|
||||
import { Card, CardBody } from '@nextui-org/card';
|
||||
import Link from 'next/link';
|
||||
|
||||
export class StarRepo extends GhActivity {
|
||||
constructor(
|
||||
readonly repoName: string,
|
||||
readonly repoUrl: string,
|
||||
time: Date
|
||||
) {
|
||||
super(time);
|
||||
}
|
||||
|
||||
toElement() {
|
||||
return (
|
||||
// <div>
|
||||
// <Link
|
||||
// href={ this.repoUrl }
|
||||
// >{ `${ GH_USER_NAME } started ${ this.repoName } at ${ this.time }` }</Link>
|
||||
// </div>
|
||||
<Card
|
||||
isHoverable={ true }
|
||||
as={ Link }
|
||||
href={ this.repoUrl }
|
||||
className={ 'bg-black/20' }
|
||||
>
|
||||
<CardBody>
|
||||
<p className={ 'text-sm' }>{ `Stared GitHub repository '${ this.repoName }'` }</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { GhActivity } from '@/src/HomePageActivities/activity';
|
||||
import { match } from 'ts-pattern';
|
||||
import { StarRepo } from '@/src/HomePageActivities/ghActivities/starRepo';
|
||||
import { GH_USER_NAME } from '@/src/HomePageActivities/constants';
|
||||
|
||||
|
||||
export const getGhActivities: () => Promise<GhActivity[]> = async () => {
|
||||
const data = await fetch(`https://api.github.com/users/${ GH_USER_NAME }/events`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
});
|
||||
return data
|
||||
.json()
|
||||
.then((ret: any): GhActivity[] => (
|
||||
ret
|
||||
.map((res: any) => (
|
||||
match(res)
|
||||
.returnType<GhActivity | undefined>()
|
||||
// star repo
|
||||
.with(
|
||||
{ type: 'WatchEvent', payload: { action: 'started' } }
|
||||
, () => (
|
||||
new StarRepo(res.repo.name, res.repo.url, res.created_at)
|
||||
))
|
||||
// to be continued
|
||||
.otherwise(() => undefined))
|
||||
)
|
||||
.filter((res: GhActivity | undefined) => res !== undefined)
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const SITE_CONFIG = {
|
||||
name: 'Atelier Lumiaethor',
|
||||
desc: ''
|
||||
};
|
||||
Reference in new issue
Block a user