mirror of
https://github.com/DefectingCat/DefectingCat.github.io
synced 2025-07-16 09:11:38 +00:00
Add tweet card
* add new hooks useUnified * fix ol li has not list style * add handle no tweets
This commit is contained in:
36
components/tweets/TweetCard.tsx
Normal file
36
components/tweets/TweetCard.tsx
Normal 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;
|
@ -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
49
lib/hooks/useUnified.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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
52
pages/tweets/admin.tsx
Normal 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
66
pages/tweets/index.tsx
Normal 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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
|
Reference in New Issue
Block a user