Add tweet card

* add new hooks useUnified
* fix ol li has not list style
* add handle no tweets
This commit is contained in:
Defectink
2022-03-06 21:24:38 +08:00
parent bf26574287
commit d2d4f2b8b1
8 changed files with 244 additions and 130 deletions

View File

@ -0,0 +1,36 @@
import { FC } from 'react';
import type { TweetsWithUser } from 'pages/tweets';
import cn from 'classnames';
import useUnified from 'lib/hooks/useUnified';
import dynamic from 'next/dynamic';
const Date = dynamic(() => import('components/DateFormater'));
interface Props {
tweet: TweetsWithUser;
}
const TweetCard: FC<Props> = ({ tweet }) => {
const postContent = useUnified(tweet.content);
return (
<>
<div
className={cn(
'bg-white dark:bg-rua-gray-800',
'md:hover:shadow-lg rounded-xl',
'p-6 transition-all duration-500',
'md:w-4/5 mx-auto lg:w-3/4'
)}
>
<h1 className="pb-2 text-gray-500">
@{tweet.Users.username}
<Date dateString={tweet.createAt} />
</h1>
<div>{postContent}</div>
</div>
</>
);
};
export default TweetCard;

View File

@ -22,7 +22,7 @@ const MainLayout: FC = ({ children }) => {
>
<aside
className={cn(
'col-span-12 px-2',
'col-span-12 px-2 sticky top-8',
'md:col-span-3 lg:col-span-2 xl:col-span-1'
)}
>

49
lib/hooks/useUnified.tsx Normal file
View File

@ -0,0 +1,49 @@
import React, { createElement, Fragment, useEffect, useMemo } from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
import rehypeHighlight from 'rehype-highlight';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import rehypeReact from 'rehype-react';
import 'highlight.js/styles/atom-one-light.css';
import xml from 'highlight.js/lib/languages/xml';
import bash from 'highlight.js/lib/languages/bash';
import dynamic from 'next/dynamic';
const RUALink = dynamic(() => import('components/RUA/RUALink'));
const PostImage = dynamic(() => import('components/post/PostImage'));
const PostIframe = dynamic(() => import('components/post/PostIframe'));
const processedContent = unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeHighlight, {
languages: { vue: xml, bash },
aliases: { bash: ['npm'] },
ignoreMissing: true,
})
.use(rehypeSlug)
.use(remarkGfm, { tableCellPadding: true })
.use(rehypeReact, {
createElement,
components: {
a: (props: any) => (
<RUALink href={props.href} isExternal>
{props.children}
</RUALink>
),
img: (props: any) => <PostImage src={props.src} />,
iframe: (props: any) => <PostIframe src={props.src} />,
},
Fragment,
});
const useUnified = (content: string) => {
const processer = useMemo(processedContent, [processedContent]);
return processer.processSync(content).result;
};
export default useUnified;

View File

@ -1,16 +1,6 @@
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import React, { createElement, Fragment, useEffect } from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
import rehypeHighlight from 'rehype-highlight';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import rehypeReact from 'rehype-react';
import React, { useEffect } from 'react';
import 'highlight.js/styles/atom-one-light.css';
import xml from 'highlight.js/lib/languages/xml';
import bash from 'highlight.js/lib/languages/bash';
import Head from 'next/head';
import cn from 'classnames';
import dynamic from 'next/dynamic';
@ -24,50 +14,23 @@ import useInView from 'lib/hooks/useInView';
import { PrismaClient, Posts, Tags } from '@prisma/client';
import PostHeadLoading from 'components/loading/PostHeadLoading';
import { useRouter } from 'next/router';
import useUnified from 'lib/hooks/useUnified';
const PostCommentLoading = dynamic(
() => import('components/loading/PostCommentLoading')
);
const Button = dynamic(() => import('components/RUA/RUAButton'));
const RUALink = dynamic(() => import('components/RUA/RUALink'));
const TableOfContent = dynamic(() => import('components/post/PostTOC'));
const PostHeader = dynamic(() => import('components/post/PostHeader'), {
loading: () => <PostHeadLoading />,
});
const PostImage = dynamic(() => import('components/post/PostImage'));
const PostIframe = dynamic(() => import('components/post/PostIframe'));
const Footer = dynamic(() => import('components/Footer'));
const PostComment = dynamic(() => import('components/post/PostComment'), {
loading: () => <PostCommentLoading />,
});
const DarkModeBtn = dynamic(() => import('components/nav/DarkModeBtn'));
const processedContent = unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeHighlight, {
languages: { vue: xml, bash },
aliases: { bash: ['npm'] },
ignoreMissing: true,
})
.use(rehypeSlug)
.use(remarkGfm, { tableCellPadding: true })
.use(rehypeReact, {
createElement,
components: {
a: (props: any) => (
<RUALink href={props.href} isExternal>
{props.children}
</RUALink>
),
img: (props: any) => <PostImage src={props.src} />,
iframe: (props: any) => <PostIframe src={props.src} />,
},
Fragment,
});
const Post = ({ post }: InferGetStaticPropsType<typeof getStaticProps>) => {
const { targetRef, inView } = useInView();
@ -81,12 +44,11 @@ const Post = ({ post }: InferGetStaticPropsType<typeof getStaticProps>) => {
if (!post) router.replace('/404');
});
const postContent = useUnified(post ? post.content : '');
if (!post) return;
const { title, index_img, content, tags, date } = post;
const postContent = processedContent.processSync(content).result;
return (
<>
<Head>

View File

@ -1,72 +0,0 @@
import { ReactElement, useState } from 'react';
import dynamic from 'next/dynamic';
import { PrismaClient } from '@prisma/client';
import { InferGetStaticPropsType } from 'next';
import Head from 'next/head';
import cn from 'classnames';
const MainLayout = dynamic(() => import('layouts/MainLayout'));
const Button = dynamic(() => import('components/RUA/RUAButton'));
const Input = dynamic(() => import('components/RUA/RUAInput'));
const Tweets = ({ tweets }: InferGetStaticPropsType<typeof getStaticProps>) => {
const [showInput, setShowInput] = useState(false);
const [showOverFlow, setShowOverFlow] = useState(true);
const handleLoginClick = () => {
if (!showInput) {
setShowInput(true);
setTimeout(() => {
setShowOverFlow(false);
}, 299);
} else {
// handle login
}
};
return (
<>
<Head>
<title>RUA - Tweets</title>
</Head>
<div>
<div className={cn('')}>
<div
className={cn(
'flex flex-col mb-4',
'text-sm md:w-1/2 transition-all',
'max-h-0 duration-300',
{ 'overflow-hidden': showOverFlow },
{ 'max-h-32': showInput }
)}
>
<Input placeholder="Username" className="py-3 mb-4" />
<Input placeholder="Password" className="py-3" type="password" />
</div>
<Button className="px-5 py-2" onClick={handleLoginClick}>
Login{showInput ? '🚀' : '🤔'}
</Button>
</div>
</div>
</>
);
};
Tweets.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Tweets;
export const getStaticProps = async () => {
const prisma = new PrismaClient();
const tweets = await prisma.tweets.findMany();
return {
props: {
tweets,
},
revalidate: 10,
};
};

52
pages/tweets/admin.tsx Normal file
View File

@ -0,0 +1,52 @@
import dynamic from 'next/dynamic';
import { ReactElement, useState } from 'react';
import cn from 'classnames';
import Head from 'next/head';
const MainLayout = dynamic(() => import('layouts/MainLayout'));
const Button = dynamic(() => import('components/RUA/RUAButton'));
const Input = dynamic(() => import('components/RUA/RUAInput'));
const Admin = () => {
const [showInput, setShowInput] = useState(false);
const [showOverFlow, setShowOverFlow] = useState(true);
const handleLoginClick = () => {
if (!showInput) {
setShowInput(true);
setTimeout(() => {
setShowOverFlow(false);
}, 299);
} else {
// handle login
}
};
return (
<>
<Head>
<title>RUA - Tweets</title>
</Head>
<div className={cn('')}>
<div
className={cn(
'flex flex-col mb-4',
'text-sm md:w-1/2 transition-all'
)}
>
<Input placeholder="Username" className="py-3 mb-4" />
<Input placeholder="Password" className="py-3" type="password" />
</div>
<Button className="px-5 py-2" onClick={handleLoginClick}>
Login{showInput ? '🚀' : '🤔'}
</Button>
</div>
</>
);
};
Admin.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Admin;

66
pages/tweets/index.tsx Normal file
View File

@ -0,0 +1,66 @@
import { ReactElement } from 'react';
import dynamic from 'next/dynamic';
import { PrismaClient, Tweets } from '@prisma/client';
import { InferGetStaticPropsType } from 'next';
import Head from 'next/head';
import 'highlight.js/styles/atom-one-light.css';
const MainLayout = dynamic(() => import('layouts/MainLayout'));
const TweetCard = dynamic(() => import('components/tweets/TweetCard'));
const TweetsPage = ({
tweets,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
<Head>
<title>RUA - Tweets</title>
</Head>
{tweets.length ? (
<div id={'tweet'} className="">
{tweets.map((tweet) => (
<TweetCard key={tweet.id} tweet={tweet} />
))}
</div>
) : (
<div className="text-xl text-gray-500">Nothing here.</div>
)}
</>
);
};
TweetsPage.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default TweetsPage;
export type TweetsWithUser = {
createAt: string;
Users: {
username: string;
};
} & Tweets;
export const getStaticProps = async () => {
const prisma = new PrismaClient();
const tweets = await prisma.tweets.findMany({
select: {
usersId: true,
Users: { select: { username: true } },
content: true,
createAt: true,
id: true,
updateAt: true,
},
});
return {
props: {
tweets: JSON.parse(JSON.stringify(tweets)) as TweetsWithUser[],
},
revalidate: 10,
};
};

View File

@ -96,7 +96,8 @@
margin: 1rem 0;
}
#write p code {
#write p code,
#tweet p code {
@apply mx-1 rounded;
}
@ -160,18 +161,27 @@
}
#write ul,
#write ol {
#write ol,
#tweet ul,
#tweet ol {
padding-left: 30px;
margin: 16px;
}
#write ul li {
#write ol,
#tweet ol {
list-style: decimal;
}
#write ul li,
#tweet ul li {
list-style-type: none;
position: relative;
margin: 8px 0;
}
#write ul li::before {
#write ul li::before,
#tweet ul li::before {
content: ' - ';
position: absolute;
font-weight: bold;
@ -180,7 +190,8 @@
transform: translate(-1.4rem, -1rem);
}
#write ul li li::before {
#write ul li li::before,
#tweet ul li li::before {
content: ' · ';
position: absolute;
font-size: 2rem;
@ -193,14 +204,16 @@
display: inline-block;
}
#write input[type='checkbox'] {
#write input[type='checkbox'],
#tweet input[type='checkbox'] {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
position: relative;
}
#write input[type='checkbox']::before {
#write input[type='checkbox']::before,
#tweet input[type='checkbox']::before {
content: '';
position: absolute;
transform: translate(calc(-1.1rem - 10px), calc(-1rem));
@ -214,7 +227,8 @@
border: solid 0.15em #c3c4d0;
}
#write input[type='checkbox']:checked::before {
#write input[type='checkbox']:checked::before,
#tweet input[type='checkbox']:checked::before {
content: '';
position: absolute;
height: 1.4rem;
@ -227,27 +241,33 @@
border: solid 0.15rem #a8c8da;
}
#write input[type='checkbox'] + p {
#write input[type='checkbox'] + p,
#tweet input[type='checkbox'] + p {
color: inherit;
}
#write input[type='checkbox']:checked + p {
#write input[type='checkbox']:checked + p,
#tweet input[type='checkbox']:checked + p {
color: #c3c4d0;
}
#write pre {
#write pre,
#tweet pre {
position: relative;
margin: 10px 0;
}
#write pre code {
#write pre code,
#tweet pre code {
@apply bg-gray-100;
padding: 6px 10px;
}
#write code,
#write tt {
@apply bg-gray-100 dark:bg-rua-gray-900 rounded-md;
#write tt,
#tweet code,
#tweet tt {
@apply bg-gray-100 rounded-md dark:bg-rua-gray-900;
padding: 0 4px;
font-size: 0.9em;
}
@ -269,7 +289,8 @@
padding: 0.8em 0.5rem;
}
#write mark {
#write mark,
#tweet mark {
color: #fff;
background-color: rgba(231, 153, 176, 0.68);
border-radius: 0.2em;