1 Commits
Author SHA1 Message Date
lumither 8938c96409 feat(frontend) add Activities.tsx 2025-02-05 13:57:20 -08:00
44 changed files with 500 additions and 250 deletions

No files matched your search

+10 -28
View File
@@ -1,34 +1,16 @@
# `Nocturne: personal website & CMS` # `Nocturne: Personal Website`
> This is the last working version 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
This is the code repo of my personal website, and [here](https://github.com/Lumither/blog-posts/) is the link of the content (blog posts etc.) repo.
content repo.
Powered by Next.js and Rust, built with passion and love Powered by Next.js and Rust, built with passion and love
*will add more here in the future > [!NOTE]
> This system is not production ready, and contains bugs, (security risk potentially).
> But have fun on it =)
## Nav: # Todos
<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,4 +1,5 @@
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};
@@ -89,24 +90,22 @@ 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 {
if drop_index( match 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,
) ) {
.is_ok() Ok(_) => info!(
{
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()
); ),
} else { Err(e) => error!(
error!( "blog content git({}) file `{}` is detected as deleted/untracked, failed to drop index: {}",
"blog content git({}) file `{}` is detected as deleted/untracked, failed to drop index",
&git_url, &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") 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 = files.to_owned(); let files_clone = dbg!(files).to_owned();
let handlers = files_clone let handlers = files_clone
.into_iter() .into_iter()
.map(|file: PathBuf| { .map(|file: PathBuf| {
+2 -1
View File
@@ -7,7 +7,8 @@ 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,4 +1,10 @@
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/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/(pages)/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/public/MarkdownRenderer'; import MarkdownRenderer from '@/app/(pages)/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/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const Pagination = ({ total }: { const Pagination = ({ total }: {
total: number, total: number,
@@ -1,11 +1,12 @@
'use server'; 'use server';
import React from 'react'; import React from 'react';
import { fetchNocturneApi } from '@/app/blog/api'; import { fetchNocturneApi } from '@/app/(pages)/blog/api';
import { MotionDiv } from '@/app/public/MotionDiv'; import BlogPostCard from '@/app/(pages)/blog/BlogPostCard';
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 }`);
@@ -16,13 +17,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' }>
<MotionDiv <motion.div
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 } />
</MotionDiv> </motion.div>
</li>) </li>)
) } ) }
</ul> </ul>
File renamed without changes.
@@ -1,4 +1,10 @@
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/blog/PostList'; import PostList from '@/app/(pages)/blog/PostList';
import Pagination from '@/app/blog/Pagination'; import Pagination from '@/app/(pages)/blog/Pagination';
import { fetchNocturneApi } from '@/app/blog/api'; import { fetchNocturneApi } from '@/app/(pages)/blog/api';
async function Blog({ async function Blog({
searchParams searchParams
@@ -1,24 +1,32 @@
import React from 'react'; 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 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 { 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 prev_post = adjacent_posts['prev']; const next_post = adjacent_posts.next;
const next_post = adjacent_posts['next']; const link = perm_link ?? `https://lumither.com/blog/post/${ post_id }`;
const link = post['meta']['link'] ?? `https://lumither.com/blog/post/${ post['meta']['id'] }`; const author = props.author ?? 'Tao';
const author = post['meta']['author'] ?? 'Tao'; const author_link = props.author_link ?? '/about/me';
const author_link = post['meta']['author_link'] ?? '/about'; const post_date = new Date(first_update);
const post_date = new Date(post['first_update']); const update_date: Date | undefined = last_update ? new Date(last_update) : undefined;
const update_date: Date | undefined = post['last_update'] ? new Date(post['last_update']) : undefined; const cc = props.cc ?? 'by-nc-sa';
const cc = post['meta']['cc'] ?? 'by-nc-sa';
return ( return (
@@ -35,7 +43,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' }>{ post['meta']['title'] }</p> <p className={ 'text-xl font-bold' }>{ 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 = {
post: any title: string;
}; subtitle: string;
summary: string | undefined;
category: string;
tags: string[];
first_update: string;
header_img: string | undefined;
}
const Headers = (props: Props) => { const Headers = (post: 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 (
<> <>
{ header_img && <div className={ `w-full overflow h-96 relative` }> { post.header_img && <div className={ `w-full overflow h-96 relative` }>
<Image <Image
alt={ 'cover picture' } alt={ 'cover picture' }
src={ header_img } src={ post.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={ header_img } src={ post.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={ 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-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={ 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` } <Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
variant={ `solid` }>{ category }</Chip> variant={ `solid` }>{ post.category }</Chip>
<p className={ `text-4xl font-bold` }> { title }</p> <p className={ `text-4xl font-bold` }> { post.title }</p>
<p className={ `text-xl` }> { sub_title }</p> <p className={ `text-xl` }> { post.subtitle }</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` }>
{ {
tags.map((tag: string, key: any) => ( post.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 = (props: 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` }>
{ first_update.toLocaleString('default', { { (new Date(post.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-row items-center text-center max-h-fit' }> <div className={ 'flex flex-col max-h-fit text-left' }>
<p className={ 'text-3xl font-bold' }>{ post['title'] }</p> <p className={ 'text-3xl font-bold text-balance' }>{ post['title'] }</p>
<div className={ 'h-full w-1 mx-2 bg-zinc-500 dark:bg-zinc-400' }></div> <div className={ 'w-1/4 h-[2px] my-2 bg-zinc-500 dark:bg-zinc-400' }></div>
<p className={ 'text-xl' }>{ post['sub_title'] }</p> <p className={ 'text-xl text-pretty break-words' }>{ post['sub_title'] }</p>
</div> </div>
</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 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.
+30
View File
@@ -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.
+43
View File
@@ -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;
-53
View File
@@ -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;
-29
View File
@@ -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;
+56
View File
@@ -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
View File
@@ -2,13 +2,14 @@ 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 Navbar from '@/components/Navbar'; import { SITE_CONFIG } from '@/src/constants';
import Footer from '@/components/Footer';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Lumitherの酒馆', openGraph: {
description: '一间位于时间夹缝中的酒馆,静候下一位旅者的到来...' siteName: SITE_CONFIG.name
},
title: SITE_CONFIG.name,
description: SITE_CONFIG.desc
}; };
export default function RootLayout({ export default function RootLayout({
@@ -20,13 +21,7 @@ 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">
<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 } { children }
<Footer />
</div>
</div>
</NextThemesProvider> </NextThemesProvider>
</body> </body>
</html> </html>
+44 -3
View File
@@ -1,10 +1,51 @@
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder'; 'use client';
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` }> <div className={ `h-dvh relative` }>
<ConstructionPlaceHolder /> <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> </div>
); );
} }
-5
View File
@@ -1,5 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export const MotionDiv = motion.div;
+2 -55
View File
@@ -3,65 +3,12 @@
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/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() { 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/public/screenSizeTrigger'; import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const switcherElements = new Map( const switcherElements = new Map(
[ [
+2 -1
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,6 +20,7 @@
"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",
@@ -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)
));
};
+4
View File
@@ -0,0 +1,4 @@
export const SITE_CONFIG = {
name: 'Atelier Lumiaethor',
desc: ''
};