feat(frontend) add Activities.tsx

This commit is contained in:
lumither committed 2025-02-05 13:57:20 -08:00
1 parent 6da6231c92
commit 8938c96409
44 files changed
+501 -253

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
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
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'flex p-7' }>
{ children }
</div>
);
}
export default Layout;
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch';
const Me = async () => {
return (
<div className={ 'p-5' }>
<MarkdownRenderer>
{ await fetchText('https://raw.githubusercontent.com/lumither/blog-posts/master/about/me.md') }
</MarkdownRenderer>
</div>
);
};
export default Me;
+58
View File
@@ -0,0 +1,58 @@
import { Card } from '@nextui-org/react';
import React from 'react';
import { CardBody } from '@nextui-org/card';
import Link from 'next/link';
const About = () => {
return (
<div className={ `flex flex-col w-full justify-center align-middle h-dvh` }>
<div className={ 'md:relative md:h-[130px] mb-4 md:mb-0' }>
<div className={ 'md:relative md:left-[20%] flex flex-col w-fit' }>
<p className={ 'text-pretty text-3xl' }>{ '讷,远道而来的' }<span
className={ 'break-keep underline decoration-sky-400 dark:decoration-sky-700 decoration-4 underline-offset-0' }>旅人</span>
</p>
<p className={ 'text-pretty md:text-2xl mt-1 md:ml-4' }>{ '愿意坐下来听个故事吗...' }</p>
<p className={ 'hidden md:block text-lg md:ml-8' }>{ '一个关于一位' }<span
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ '追寻' }<span
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ ',探索' }<span
className={ 'text-gray-700 dark:text-gray-400 text-sm transition-colors' }>{ '█' }</span>{ '的故事' }
</p>
</div>
</div>
<div className={ `flex justify-center w-full h-[60%] md:h-[40%]` }>
<div
className={ `flex flex-col md:flex-row w-full md:w-[85%] lg:w-[90%] xl:w-[85%] p-4 gap-x-7 gap-y-5` }>
<Card
isHoverable
className={ 'w-full h-[45%] md:h-full' }
as={ Link }
href={ '/about/me' }
isPressable
>
<CardBody>
<div className={ 'h-full flex flex-col justify-center' }>
<p className={ 'text-center text-3xl h-fit' }>关于我</p>
</div>
</CardBody>
</Card>
<Card
isHoverable
className={ 'w-full h-[45%] md:h-full' }
as={ Link }
href={ '/about/website' }
isPressable
>
<CardBody>
<div className={ 'h-full flex flex-col justify-center' }>
<p className={ 'text-center text-3xl h-fit' }>关于这个网站</p>
</div>
</CardBody>
</Card>
</div>
</div>
<p className={ 'text-center' }>*目前还没想到如何设计这一页,先摸鱼会(笑)</p>
</div>
);
};
export default About;
@@ -0,0 +1,14 @@
import React from 'react';
import MarkdownRenderer from '@/app/(pages)/public/MarkdownRenderer';
import { fetchText } from '@/app/fetch';
const Website = async () => {
return (
<div className={ 'p-5' }>
<MarkdownRenderer>
{ await fetchText('https://raw.githubusercontent.com/lumither/blog-posts/master/about/website.md') }
</MarkdownRenderer>
</div>
);
};
export default Website;
@@ -0,0 +1,92 @@
import React from 'react';
import { Card, CardBody, CardFooter } from '@nextui-org/card';
import { Chip } from '@nextui-org/react';
import Image from 'next/image';
import Link from 'next/link';
import { MdCalendarMonth, MdNorthEast } from 'react-icons/md';
type Props = {
post: JSON
}
function BlogPostCard({ post }: Props) {
let title = (post as any)['title'];
let sub_title = (post as any)['sub_title'];
// let summary = (post as any)['summary'];
let category = (post as any)['category'];
let tags = (post as any)['tags'];
// let last_update = new Date((post as any)['last_update']);
let first_update = new Date((post as any)['first_update']);
let post_id = (post as any)['post_id'];
let header_img = ((post as any)['header_img'] as string | null);
return (
<div className={ `w-full` }>
<Card as={ Link } href={ `/blog/post/${ post_id }` }
className={ `group` }
isPressable
isHoverable
>
{ header_img && <div className={ `w-full overflow h-80 relative` }>
<Image
alt={ 'cover picture' }
src={ header_img }
className={ `w-full object-cover rounded-b-none z-10` }
loading={ 'eager' }
fill
></Image>
<Image
alt={ 'cover picture shadow' }
src={ header_img }
loading={ 'eager' }
className={ `w-full object-cover rounded-b-none scale-y-110 blur-2xl` }
fill
></Image>
</div> }
<CardBody className={ `overflow-visible` }>
<div className={ `flex flex-row justify-between` }>
<div className={ `mx-2 ${ header_img ? '-mt-6' : 'mt-2' }` }>
<Chip className={ `mb-2 z-30 text-lg` } radius={ `sm` } size={ 'lg' } color={ `secondary` }
variant={ `solid` }>{ category }</Chip>
<p className={ `text-4xl font-bold` }> { title }</p>
<p className={ `text-xl` }> { sub_title }</p>
</div>
<MdNorthEast className={ `mt-1 mr-2` } size={ `40px` } />
</div>
</CardBody>
<CardFooter className={ 'pt-0' }>
<div className={ `flex flex-row flex-wrap justify-between w-full` }>
<div>
<ul className={ `flex flex-row flex-wrap` }>
{
tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
</li>
))
}
</ul>
</div>
<div className={ `flex flex-row place-content-end mb-1 items-center mx-1` }>
<MdCalendarMonth />
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
{ first_update.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }
</p>
</div>
</div>
</CardFooter>
</Card>
</div>
);
}
export default BlogPostCard;
+1
View File
@@ -0,0 +1 @@
// todo: create component
+37
View File
@@ -0,0 +1,37 @@
'use client';
import React from 'react';
import { Pagination as NextUIPagination } from '@nextui-org/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const Pagination = ({ total }: {
total: number,
}) => {
const parsePage = Number(useSearchParams().get('page'));
const currPage = parsePage > 0 ? parsePage : 1;
const router = useRouter();
const isMobile = useScreenSizeTrigger('md');
return (
<div className={ 'w-full p-4 pt-6 flex justify-center' }>
<NextUIPagination
initialPage={ currPage }
total={ total }
siblings={ isMobile ? 0 : 1 }
size={ isMobile ? 'sm' : 'md' }
showControls
showShadow
color={ 'default' }
variant={ 'light' }
onChange={ (page) => {
router.push(`?page=${ page }`, { scroll: false });
} }
/>
</div>
);
};
export default Pagination;
+43
View File
@@ -0,0 +1,43 @@
'use server';
import React from 'react';
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 }`);
const posts = await res.json();
return (
// blog list
<div className={ 'w-full' }>
<ul className={ 'flex flex-col items-center w-full space-y-7' }>
{ posts.map((post: JSON, key: number) => (
<li key={ key } className={ 'w-full' }>
<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 } />
</motion.div>
</li>)
) }
</ul>
</div>
);
} catch (e: any) {
return (
// todo: use Error.tsx
<Card className={ `w-full` }>
<CardBody>
Fatal: { e.toString() }
</CardBody>
</Card>);
}
};
export default PostList;
+21
View File
@@ -0,0 +1,21 @@
const LOCAL_ENDPOINT = process.env.API_LOCAL_URL;
const REMOTE_ENDPOINT = process.env.API_REMOTE_URL;
const NOCTURNE_ENDPOINT = ((isServerSide = true) => {
if (process.env.NODE_ENV === 'production') {
if (isServerSide) {
return LOCAL_ENDPOINT;
} else {
return REMOTE_ENDPOINT;
}
} else {
return LOCAL_ENDPOINT;
}
});
export async function fetchNocturneApi(path: string, isServerSideRequest = true) {
return await fetch(`${ NOCTURNE_ENDPOINT(isServerSideRequest) }${ path }`, {
cache: 'no-cache'
});
}
+23
View File
@@ -0,0 +1,23 @@
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
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'flex flex-auto p-7 pl-2 lg:pl-7 w-full justify-center' }>
<main className={ 'w-full' }>
{ children }
</main>
</div>
);
}
export default Layout;
+24
View File
@@ -0,0 +1,24 @@
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
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const parsePage = Number((await searchParams)?.page);
const currPage = parsePage > 0 ? parsePage : 1;
const ret = await fetchNocturneApi(`/blog/get_page_count`);
const pageCount = (await ret.json() as any)['res'] as number;
return (
<div className={ 'w-full min-w-full space-y-4' }>
<PostList page={ currPage } />
<Pagination total={ pageCount } />
</div>
);
}
export default Blog;
@@ -0,0 +1,21 @@
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@nextui-org/react';
import { IoArrowBackOutline } from 'react-icons/io5';
const ButtonGoBack = () => {
const router = useRouter();
return (
<Button
startContent={ <IoArrowBackOutline /> }
variant={ 'light' }
onClick={ () => router.back() }
>
{ 'Back' }
</Button>
);
};
export default ButtonGoBack;
@@ -0,0 +1,104 @@
import React from 'react';
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_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 { adjacent_posts, perm_link, post_id, first_update, last_update, title } = props;
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 (
<div className={ `w-full` }>
<div className={ 'flex transition bg-zinc-300/80 dark:bg-gray-600/25' }>
<div className={ `p-8 w-full overflow-hidden` }>
<div className={ 'relative' }>
<FaCreativeCommons
size={ '160px' }
className={ 'absolute right-0 top-0 -mt-[4.8rem] -mr-16 opacity-30 brightness-50 ' }
/>
</div>
<div className={ 'flex flex-col space-y-4 relative' }>
<div className={ 'z-[2]' }>
<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>
</div>
<div className={ 'z-[2] flex flex-row flex-wrap gap-x-7 gap-y-2' }>
<div>
<p className={ 'text-lg font-bold' }>Author</p>
<Link href={ author_link }>
<p className={ 'underline decoration-indigo-500 dark:decoration-sky-500' }>{ author }</p>
</Link>
</div>
<div>
<p className={ 'text-lg font-bold' }>Posted on</p>
<p>{ post_date.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }</p>
</div>
{ update_date &&
<div>
<p className={ 'text-lg font-bold' }>Updated on</p>
<p>{ update_date.toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }</p>
</div>
}
<div>
<p className={ 'text-lg font-bold' }>Published under</p>
<Link href={ `https://creativecommons.org/licenses/${ cc }/4.0/` }>
<p className={ 'underline decoration-indigo-500 dark:decoration-sky-500' }>{ `CC ${ cc.toUpperCase() } 4.0` }</p>
</Link>
</div>
</div>
</div>
</div>
</div>
<div className={ `flex justify-center` }>
<div className={ `w-full shadow-2xl ` }>
<div className={ `flex flex-col md:flex-row w-full` }>
{
next_post &&
<MinifyPostCard post={ next_post } desc={ 'prev' }></MinifyPostCard>
}
{
prev_post &&
<MinifyPostCard post={ prev_post } desc={ 'next' }></MinifyPostCard>
}
</div>
</div>
</div>
</div>
);
};
export default Footer;
@@ -0,0 +1,80 @@
import React from 'react';
import Image from 'next/image';
import { Chip } from '@nextui-org/react';
import { MdCalendarMonth } from 'react-icons/md';
// type Props = {
// post: any
// };
type Props = {
title: string;
subtitle: string;
summary: string | undefined;
category: string;
tags: string[];
first_update: string;
header_img: string | undefined;
}
const Headers = (post: Props) => {
return (
<>
{ post.header_img && <div className={ `w-full overflow h-96 relative` }>
<Image
alt={ 'cover picture' }
src={ post.header_img }
className={ `w-full object-cover rounded-b-none z-[5]` }
fill
></Image>
<Image
alt={ 'cover picture' }
src={ post.header_img }
className={ `w-full relative scale-110 blur-3xl object-cover rounded-b-none` }
fill
></Image>
</div> }
<div
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={ post.header_img ? `p-4` : 'p-4 pt-2' }>
<Chip className={ `mb-2` } radius={ `sm` } color={ `secondary` }
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` }>
{
post.tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
</li>
))
}
</ul>
</div>
<div className={ `flex flex-row place-content-end mb-1 mr-6 items-center mx-1` }>
<MdCalendarMonth />
<p className={ `text-zinc-500 dark:text-zinc-400 ml-1` }>
{ (new Date(post.first_update)).toLocaleString('default', {
month: `long`,
day: `numeric`,
year: `numeric`
}) }
</p>
</div>
</div>
</div>
</>
);
};
export default Headers;
@@ -0,0 +1,58 @@
import React from 'react';
import { Card, CardFooter, CardHeader } from '@nextui-org/card';
import Image from 'next/image';
import Link from 'next/link';
type Props = {
post: any
desc: string
}
const MinifyPostCard = (props: Props) => {
const { post, desc } = props;
const img_url = post['header_img'] as string;
return (
<div className={ 'w-full transition hover:brightness-75' }>
<Card isFooterBlurred
className="w-full h-[300px] shadow-none border-0 rounded-none overflow-hidden"
as={ Link }
isPressable
href={ `/blog/post/${ post['id'] }` }
>
<CardHeader className="absolute z-10 top-1 flex-col items-start">
<p className={ `text-tiny uppercase font-bold ${ img_url ? 'text-white/60' : 'text-black/60 dark:text-white/60' }` }>{ desc }</p>
</CardHeader>
{ img_url &&
<div className={ 'h-full' }>
<div className={ 'relative h-[300px] overflow-hidden' }>
<Image
className={ 'object-cover object-center ' }
src={ img_url }
alt={ post.title }
fill
/>
</div>
<CardFooter
className="absolute transition-colors bg-zinc-300/80 dark:bg-gray-600/25 dark:backdrop-brightness-75 bottom-0 justify-between rounded-none">
<div>
<p className="text-2xl font-bold">{ post['title'] }</p>
<p className="">{ post['sub_title'] }</p>
</div>
</CardFooter>
</div>
}
{ !img_url &&
<div className={ 'h-full grid p-6 place-content-center' }>
<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>
}
</Card>
</div>
);
};
export default MinifyPostCard;
@@ -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;
+21
View File
@@ -0,0 +1,21 @@
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
}: {
children: React.ReactNode;
}) {
return (
<div className={ 'p-7' }>
{ children }
</div>
);
}
export default Layout;
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import { fetchJson } from '@/app/fetch';
import { Card, CardBody } from '@nextui-org/react';
import Image from 'next/image';
import Link from 'next/link';
export default async function Friends() {
const list = await fetchJson('https://raw.githubusercontent.com/Lumither/friends/master/friends.json');
const listLength = list.length;
const listMaxIdx = listLength - 1;
const listIsOdd = listLength % 2 !== 0;
return (
<div>
<div className={ `min-h-dvh pb-10` }>
<div className={ 'p-7 pt-[10%] md:pt-[20%]' }>
<div className={ 'flex flex-col md:flex-row gap-x-3 justify-center' }>
<p className={ 'text-3xl text-center text-balance' }>海内存知己</p>
<p className={ 'text-3xl text-center text-balance' }>天涯若比邻</p>
</div>
<p className={ 'text-center text-balance text-zinc-500 dark:text-zinc-400' }>{ 'Though far apart, kin at heart' }</p>
</div>
<div className={ 'flex w-full h-full justify-center pt-3' }>
<ul className={ 'grid grid-cols-2 gap-x-6 gap-y-3 md:max-w-[90%] xl:max-w-[85%] w-full' }>
{
list.map((friend: any, key: number) => (
<li
key={ key }
className={ (key === listMaxIdx && listIsOdd) ? 'col-span-2 flex justify-center' : 'col-span-2 md:col-span-1' }
>
<Card
isHoverable
isPressable
shadow={ 'none' }
as={ Link }
href={ friend['url'] }
className={ 'dark:bg-[#282830] w-full md:w-fit max-w-full shadow' }
>
<CardBody className={ 'overflow-visible pb-0.5' }>
<div className={ 'flex flex-row' }>
<div className={ 'flex p-2' }>
<div
className={ 'h-[80px] w-[80px] relative rounded-2xl' }>
<Image
alt="avatar"
className="object-cover rounded-2xl z-10"
fill
unoptimized
src={ friend['avatar'] }
/>
<Image
alt="avatar"
className="relative object-cover rounded-2xl scale-110 blur-3xl"
fill
unoptimized
src={ friend['avatar'] }
/>
</div>
</div>
<div className={ 'p-2 flex-col max-w-full' }>
<p className={ 'text-xl' }>{ friend['title'] }</p>
{ (friend['name'] === undefined || friend['name'].isNull) ??
<p className={ 'text-zinc-600 dark:text-zinc-400' }>{ `@ ${ friend['name'] }` }</p> }
{ (friend['desc'] === undefined || friend['desc'].isNull) ??
<p className={ 'text-sm text-pretty text-zinc-600 dark:text-zinc-300 mt-1' }>{ friend['desc'] }</p> }
</div>
</div>
</CardBody>
<div className={ 'h-2 relative w-full' }>
<Image
alt="theme bar"
draggable={ false }
className="object-cover object-center scale-110 blur-lg"
fill
unoptimized
src={ friend['avatar'] }
/>
</div>
</Card>
</li>
))
}
</ul>
</div>
</div>
<p className={ 'text-center' }>想要成为好伙伴嘛,向<span
className={ 'text-blue-800 dark:text-blue-500' }><Link
href={ 'https://github.com/Lumither/friends/' }>这里</Link></span>发起PR吧
</p>
</div>
);
}
+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;
@@ -0,0 +1,64 @@
import React from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkToc from 'remark-toc';
import remarkFrontmatter from 'remark-frontmatter';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
const MarkdownRenderer = ({ children }: { children: React.ReactNode }) => {
return (
<div className={ 'w-full' }>
<Markdown
className={ `max-w-full text-justify text-pretty prose prose-sm prose-neutral dark:prose-invert
md:prose-md lg:prose-lg prose-code:overflow-x-scroll`
}
remarkPlugins={ [ remarkGfm, remarkToc, remarkFrontmatter ] }
rehypePlugins={ [ rehypeRaw, rehypeSanitize ] }
components={ {
pre(props) {
const { node, className, children, ...rest } = props;
if ((children as any)['type'] === 'code') {
const match = /language-(\w+)/.exec((children as any)['props']['className'] || '');
let lang = match ? match[1] : 'text';
return (
<pre className={ `not-prose` }>
<SyntaxHighlighter
// @ts-ignore
style={ oneDark }
language={ lang }
showLineNumbers
// wrapLongLines
// wrapLines
classNames={ className }
PreTag="div"
{ ...rest }
>
{
String((children as any)['props']['children']).replace(/\n$/, '')
}
</SyntaxHighlighter>
</pre>
);
} else {
return (
<pre className={ `not-prose` }>
<code className={ className } { ...props }>
{ children }
</code>
</pre>
);
}
}
} }
>
{
children as any
}
</Markdown>
</div>
);
};
export default MarkdownRenderer;
Whitespace-only changes.
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
const screenSize = new Map<string, number>(
[
[ 'sm', 640 ],
[ 'md', 768 ],
[ 'lg', 1024 ],
[ 'xl', 1280 ],
[ '2xl', 2536 ]
]
);
export const useScreenSizeTrigger = (size: number | string) => {
const criticalSize = (typeof size === 'string') ? (screenSize.get(size) ?? 768) : size;
const [ isCritical, setIsCritical ] = useState(false);
useEffect(() => {
const updateMobileWidth = () => {
if (window.innerWidth < criticalSize) {
setIsCritical(true);
} else {
setIsCritical(false);
}
};
updateMobileWidth();
window.addEventListener('resize', updateMobileWidth);
return () => {
window.removeEventListener('resize', updateMobileWidth);
};
});
return isCritical;
};
@@ -0,0 +1,7 @@
export type Result<T, E> = {
ok: true;
value: T;
} | {
ok: false;
error: E;
};
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import ConstructionPlaceHolder from '@/components/ConstructionPlaceHolder';
const Search = () => {
return (
<div className={ `h-dvh` }>
<ConstructionPlaceHolder />
</div>
);
};
export default Search;