1 Commits
Author SHA1 Message Date
lumither d6c7602123 git(global) create release branch 2025-04-13 03:52:03 -07:00
44 changed files with 251 additions and 501 deletions

No files matched your search

+28 -10
View File
@@ -1,16 +1,34 @@
# `Nocturne: Personal Website` # `Nocturne: personal website & CMS`
This is the code repo of my [personal website](https://lumither.com), > This is the last working version
and [here](https://github.com/Lumither/blog-posts/) is the link to the
content (blog posts etc.) repo. This is the code repo of my personal website, and [here](https://github.com/Lumither/blog-posts/) is the link of the
content repo.
Powered by Next.js and Rust, built with passion and love Powered by Next.js and Rust, built with passion and love
> [!NOTE] *will add more here in the future
> This system is not production ready, and contains bugs, (security risk potentially).
> But have fun on it =)
# Todos ## 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
- [ ] the todo list lol
- [ ] and much more...
@@ -1,5 +1,4 @@
use std::env; use std::env;
use std::error::Error;
use std::fs::remove_dir_all; use std::fs::remove_dir_all;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -90,22 +89,24 @@ pub fn task(db_connection: &Pool<Postgres>) {
for update in updates { for update in updates {
if [Delta::Deleted, Delta::Untracked].contains(&update.status) { if [Delta::Deleted, Delta::Untracked].contains(&update.status) {
if let Some(path) = &update.old_path { if let Some(path) = &update.old_path {
match drop_index( if drop_index(
&get_md_file_basename( &get_md_file_basename(
path.file_name().unwrap().to_str().unwrap(), path.file_name().unwrap().to_str().unwrap(),
), ),
db_connection, db_connection,
) { )
Ok(_) => info!( .is_ok()
{
info!(
"blog content git({}) file `{}` is detected as deleted/untracked, index dropped", "blog content git({}) file `{}` is detected as deleted/untracked, index dropped",
&git_url, &git_url,
&path.display() &path.display()
), );
Err(e) => error!( } else {
"blog content git({}) file `{}` is detected as deleted/untracked, failed to drop index: {}", error!(
"blog content git({}) file `{}` is detected as deleted/untracked, failed to drop index",
&git_url, &git_url,
&path.display(), &path.display()
e
) )
} }
} }
@@ -113,8 +114,7 @@ pub fn task(db_connection: &Pool<Postgres>) {
if path.components().any(|comp| comp.as_os_str() == "posts") if path.components().any(|comp| comp.as_os_str() == "posts")
&& path.extension().is_some_and(|ext| ext == "md") && 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>) { pub fn index_files(files: &[PathBuf], db_conn: &Pool<Postgres>) {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
let files_clone = dbg!(files).to_owned(); let files_clone = files.to_owned();
let handlers = files_clone let handlers = files_clone
.into_iter() .into_iter()
.map(|file: PathBuf| { .map(|file: PathBuf| {
+1 -2
View File
@@ -7,8 +7,7 @@ pub mod check_update;
pub fn get_tasks(db_connection: Pool<Postgres>) -> Vec<Box<dyn Task>> { pub fn get_tasks(db_connection: Pool<Postgres>) -> Vec<Box<dyn Task>> {
let db_conn_clone = db_connection.clone(); let db_conn_clone = db_connection.clone();
vec![ 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() .unwrap()
.to_task(), .to_task(),
] ]
@@ -1,55 +0,0 @@
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;
@@ -1,70 +0,0 @@
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;
-30
View File
@@ -1,30 +0,0 @@
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;
-43
View File
@@ -1,43 +0,0 @@
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,10 +1,4 @@
import React from 'react'; 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({ function Layout({
children children
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch'; import { fetchText } from '@/app/fetch';
const Me = async () => { const Me = async () => {
File renamed without changes.
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch'; import { fetchText } from '@/app/fetch';
const Website = async () => { const Website = async () => {
File renamed without changes.
@@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import { Pagination as NextUIPagination } from '@nextui-org/react'; import { Pagination as NextUIPagination } from '@nextui-org/react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
const Pagination = ({ total }: { const Pagination = ({ total }: {
total: number, total: number,
@@ -1,12 +1,11 @@
'use server'; 'use server';
import React from 'react'; import React from 'react';
import { fetchNocturneApi } from '@/app/(pages)/blog/api'; import { fetchNocturneApi } from '@/app/blog/api';
import BlogPostCard from '@/app/(pages)/blog/BlogPostCard'; import { MotionDiv } from '@/app/public/MotionDiv';
import BlogPostCard from '@/app/blog/BlogPostCard';
import { Card, CardBody } from '@nextui-org/card'; import { Card, CardBody } from '@nextui-org/card';
import * as motion from 'motion/react-client';
const PostList = async ({ page }: { page: number }) => { const PostList = async ({ page }: { page: number }) => {
try { try {
const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`); const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`);
@@ -17,13 +16,13 @@ const PostList = async ({ page }: { page: number }) => {
<ul className={ 'flex flex-col items-center w-full space-y-7' }> <ul className={ 'flex flex-col items-center w-full space-y-7' }>
{ posts.map((post: JSON, key: number) => ( { posts.map((post: JSON, key: number) => (
<li key={ key } className={ 'w-full' }> <li key={ key } className={ 'w-full' }>
<motion.div <MotionDiv
initial={ { y: 20, opacity: 0 } } initial={ { y: 20, opacity: 0 } }
animate={ { y: 0, opacity: 1 } } animate={ { y: 0, opacity: 1 } }
transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } } transition={ { ease: 'easeInOut', duration: 0.5, delay: key * .2 + .25 } }
> >
<BlogPostCard post={ post } /> <BlogPostCard post={ post } />
</motion.div> </MotionDiv>
</li>) </li>)
) } ) }
</ul> </ul>
File renamed without changes.
@@ -1,10 +1,4 @@
import React from 'react'; 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({ function Layout({
children children
@@ -1,6 +1,6 @@
import PostList from '@/app/(pages)/blog/PostList'; import PostList from '@/app/blog/PostList';
import Pagination from '@/app/(pages)/blog/Pagination'; import Pagination from '@/app/blog/Pagination';
import { fetchNocturneApi } from '@/app/(pages)/blog/api'; import { fetchNocturneApi } from '@/app/blog/api';
async function Blog({ async function Blog({
searchParams searchParams
@@ -1,32 +1,24 @@
import React from 'react'; import React from 'react';
import MinifyPostCard from '@/app/(pages)/blog/post/[id]/MinifyPostCard'; import MinifyPostCard from '@/app/blog/post/[id]/MinifyPostCard';
import Link from 'next/link'; import Link from 'next/link';
import { FaCreativeCommons } from 'react-icons/fa6'; import { FaCreativeCommons } from 'react-icons/fa6';
type Props = { 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 Footer = (props: Props) => {
const { adjacent_posts, perm_link, post_id, first_update, last_update, title } = props; const { post } = props;
const prev_post = adjacent_posts.prev; const adjacent_posts = post['adj'];
const next_post = adjacent_posts.next; const prev_post = adjacent_posts['prev'];
const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`; const next_post = adjacent_posts['next'];
const author = props.author ?? 'Tao'; const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`;
const author_link = props.author_link ?? '/about/me'; const author = post['meta']['author'] ?? 'Tao';
const post_date = new Date(first_update); const author_link = post['meta']['author_link'] ?? '/about';
const update_date: Date | undefined = last_update ? new Date(last_update) : undefined; const post_date = new Date(post['first_update']);
const cc = props.cc ?? 'by-nc-sa'; const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined;
const cc = post['meta']['cc'] ?? 'by-nc-sa';
return ( return (
@@ -43,7 +35,7 @@ const Footer = (props: Props) => {
<div className={ 'flex flex-col space-y-4 relative' }> <div className={ 'flex flex-col space-y-4 relative' }>
<div className={ 'z-[2]' }> <div className={ 'z-[2]' }>
<p className={ 'text-xl font-bold' }>{ title }</p> <p className={ 'text-xl font-bold' }>{ post['meta']['title'] }</p>
<Link href={ link }> <Link href={ link }>
<p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p> <p className={ 'text-sm underline decoration-indigo-500 dark:decoration-sky-500' }>{ link }</p>
</Link> </Link>
@@ -3,57 +3,57 @@ import Image from 'next/image';
import { Chip } from '@nextui-org/react'; import { Chip } from '@nextui-org/react';
import { MdCalendarMonth } from 'react-icons/md'; import { MdCalendarMonth } from 'react-icons/md';
// type Props = {
// post: any
// };
type Props = { type Props = {
title: string; post: any
subtitle: string; };
summary: string | undefined;
category: string;
tags: string[];
first_update: string;
header_img: string | undefined;
}
const Headers = (post: Props) => { 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;
return ( return (
<> <>
{ post.header_img && <div className={ `w-full overflow h-96 relative` }> { header_img && <div className={ `w-full overflow h-96 relative` }>
<Image <Image
alt={ 'cover picture' } alt={ 'cover picture' }
src={ post.header_img } src={ header_img }
className={ `w-full object-cover rounded-b-none z-[5]` } className={ `w-full object-cover rounded-b-none z-[5]` }
fill fill
></Image> ></Image>
<Image <Image
alt={ 'cover picture' } alt={ 'cover picture' }
src={ post.header_img } src={ header_img }
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` } className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
fill fill
></Image> ></Image>
</div> } </div> }
<div <div
className={ post.header_img ? className={ header_img ?
'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30' 'mx-8 -mt-16 backdrop-blur transition bg-zinc-300/80 dark:bg-gray-600/25 rounded-2xl z-30'
: 'mx-6 mt-6' : 'mx-6 mt-6'
}> }>
<div className={ post.header_img ? `p-4` : 'p-4 pt-2' }> <div className={ header_img ? `p-4` : 'p-4 pt-2' }>
<Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` } <Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
variant={ `solid` }>{ post.category }</Chip> variant={ `solid` }>{ category }</Chip>
<p className={ `text-4xl font-bold` }> { post.title }</p> <p className={ `text-4xl font-bold` }> { title }</p>
<p className={ `text-xl` }> { post.subtitle }</p> <p className={ `text-xl` }> { sub_title }</p>
</div> </div>
<div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }> <div className={ `mx-2 -mt-2 mb-2 flex flex-row flex-wrap justify-between w-full` }>
<div> <div>
<ul className={ `flex flex-row flex-wrap` }> <ul className={ `flex flex-row flex-wrap` }>
{ {
post.tags.map((tag: string, key: any) => ( tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }> <li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip> <Chip>{ tag }</Chip>
</li> </li>
@@ -64,7 +64,7 @@ const Headers = (post: Props) => {
<div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }> <div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }>
<MdCalendarMonth /> <MdCalendarMonth />
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }> <p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
{ (new Date(post.first_update)).toLocaleString('default', { { first_update.toLocaleString('default', {
month: `long`, month: `long`,
day: `numeric`, day: `numeric`,
year: `numeric` year: `numeric`
@@ -43,10 +43,10 @@ const MinifyPostCard = (props: Props) => {
} }
{ !img_url && { !img_url &&
<div className={ 'h-full grid p-6 place-content-center' }> <div className={ 'h-full grid p-6 place-content-center' }>
<div className={ 'flex flex-col max-h-fit text-left' }> <div className={ 'flex flex-row items-center text-center max-h-fit' }>
<p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p> <p className={ 'text-3xl font-bold' }>{ post['title'] }</p>
<div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div> <div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div>
<p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p> <p className={ 'text-xl' }>{ post['sub_title'] }</p>
</div> </div>
</div> </div>
} }
+53
View File
@@ -0,0 +1,53 @@
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;
+29
View File
@@ -0,0 +1,29 @@
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;
-56
View File
@@ -1,56 +0,0 @@
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` } />
}
];
@@ -1,10 +1,4 @@
import React from 'react'; 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({ function Layout({
children children
File renamed without changes.
+12 -7
View File
@@ -2,14 +2,13 @@ import type { Metadata } from 'next';
import './globals.css'; import './globals.css';
import React from 'react'; import React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { SITE_CONFIG } from '@/src/constants'; import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
export const metadata: Metadata = { export const metadata: Metadata = {
openGraph: { title: 'Lumitherの酒馆',
siteName: SITE_CONFIG.name description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...'
},
title: SITE_CONFIG.name,
description: SITE_CONFIG.desc
}; };
export default function RootLayout({ export default function RootLayout({
@@ -21,7 +20,13 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }> <body className={ `transition duration-300 dark:bg-[#282830] min-h-screen` }>
<NextThemesProvider attribute="class" defaultTheme="system"> <NextThemesProvider attribute="class" defaultTheme="system">
{ children } <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> </NextThemesProvider>
</body> </body>
</html> </html>
+3 -44
View File
@@ -1,51 +1,10 @@
'use client'; import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
import React from 'react'; 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() { 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 ( return (
<div className={ `h-dvh relative` }> <div className={ `h-dvh` }>
<ul className={ `flex flex-col space-y-2 w-full` }> <ConstructionPlaceHolder />
{
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> </div>
); );
} }
+5
View File
@@ -0,0 +1,5 @@
'use client';
import { motion } from 'framer-motion';
export const MotionDiv = motion.div;
File renamed without changes.
+55 -2
View File
@@ -3,12 +3,65 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from '@nextui-org/react'; import { Button } from '@nextui-org/react';
import Image from 'next/image'; import Image from 'next/image';
import {
IoFlask,
IoHome,
IoInformationCircle,
IoLink,
IoLogoGithub,
IoMail,
IoPencil,
IoSearch
} from 'react-icons/io5';
import Link from 'next/link'; import Link from 'next/link';
import ThemeSwitcher from '@/components/ThemeSwitcher'; import ThemeSwitcher from '@/components/ThemeSwitcher';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/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() { function Navbar() {
+1 -1
View File
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react'; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md'; import { MdOutlineDarkMode, MdOutlineDesktopWindows, MdOutlineLightMode } from 'react-icons/md';
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/public/screenSizeTrigger';
const switcherElements = new Map( const switcherElements = new Map(
[ [
+1 -2
View File
@@ -11,8 +11,8 @@
"dependencies": { "dependencies": {
"@nextui-org/react": "^2.4.6", "@nextui-org/react": "^2.4.6",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"framer-motion": "^11.3.24",
"lxgw-wenkai-webfont": "^1.7.0", "lxgw-wenkai-webfont": "^1.7.0",
"motion": "^12.3.1",
"next": "^15.0.3", "next": "^15.0.3",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -20,7 +20,6 @@
"react-icons": "^5.2.1", "react-icons": "^5.2.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-use": "^17.6.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
@@ -1,18 +0,0 @@
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);
}
}
@@ -1 +0,0 @@
export const GH_USER_NAME = 'Lumither';
@@ -1,34 +0,0 @@
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>
);
}
}
@@ -1,32 +0,0 @@
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)
));
};
-4
View File
@@ -1,4 +0,0 @@
export const SITE_CONFIG = {
name: 'Atelier Lumiaethor',
desc: ''
};