mirror of https://github.com/sunface/rust-course
parent
74b02b029e
commit
c5205e7e98
@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
chakra,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
useColorModeValue,
|
||||||
|
useDisclosure,
|
||||||
|
useUpdateEffect,
|
||||||
|
Heading,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Text
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import { useViewportScroll } from "framer-motion"
|
||||||
|
import React from "react"
|
||||||
|
import { SearchIcon } from "@chakra-ui/icons"
|
||||||
|
import DarkMode from "components/dark-mode"
|
||||||
|
import AccountMenu from "components/account-menu"
|
||||||
|
import { FaGithub, FaTwitter, FaUserPlus } from "react-icons/fa"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function HeaderContent() {
|
||||||
|
const mobileNav = useDisclosure()
|
||||||
|
|
||||||
|
const mobileNavBtnRef = React.useRef<HTMLButtonElement>()
|
||||||
|
|
||||||
|
useUpdateEffect(() => {
|
||||||
|
mobileNavBtnRef.current?.focus()
|
||||||
|
}, [mobileNav.isOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}>
|
||||||
|
<HStack spacing="2">
|
||||||
|
<Heading size="md">Sunface的博客</Heading>
|
||||||
|
<Button colorScheme="teal" variant="outline" leftIcon={<FaUserPlus />}>Follow</Button>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
|
||||||
|
<HStack
|
||||||
|
color={useColorModeValue("gray.500", "gray.400")}
|
||||||
|
spacing="2"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
fontSize="lg"
|
||||||
|
variant="ghost"
|
||||||
|
color="current"
|
||||||
|
_focus={null}
|
||||||
|
onClick={() => alert('search in this blog')}
|
||||||
|
icon={<SearchIcon />}
|
||||||
|
aria-label="search in this blog"
|
||||||
|
/>
|
||||||
|
<DarkMode />
|
||||||
|
<AccountMenu />
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
<Flex w="100%" align="center" justify="space-between" px={{ base: "6", md: "10" }} mt="2">
|
||||||
|
<HStack spacing="4">
|
||||||
|
<Text fontSize="1.1rem" fontWeight="600">Home</Text>
|
||||||
|
<Text fontSize="1.1rem">Badges</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack
|
||||||
|
color={useColorModeValue("gray.500", "gray.400")}
|
||||||
|
spacing="2"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
fontSize="1.2rem"
|
||||||
|
aria-label="go to github"
|
||||||
|
variant="ghost"
|
||||||
|
color="current"
|
||||||
|
_focus={null}
|
||||||
|
icon={<FaGithub />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
fontSize="1.2rem"
|
||||||
|
aria-label="go to twitter"
|
||||||
|
variant="ghost"
|
||||||
|
color="current"
|
||||||
|
_focus={null}
|
||||||
|
icon={<FaTwitter />}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
<Divider mt="2"/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostNav(props) {
|
||||||
|
const ref = React.useRef<HTMLHeadingElement>()
|
||||||
|
const [y, setY] = React.useState(0)
|
||||||
|
const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
|
||||||
|
|
||||||
|
const { scrollY } = useViewportScroll()
|
||||||
|
React.useEffect(() => {
|
||||||
|
return scrollY.onChange(() => setY(scrollY.get()))
|
||||||
|
}, [scrollY])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<chakra.header
|
||||||
|
ref={ref}
|
||||||
|
shadow={y > height ? "sm" : undefined}
|
||||||
|
transition="box-shadow 0.2s"
|
||||||
|
top="0"
|
||||||
|
zIndex="3"
|
||||||
|
left="0"
|
||||||
|
right="0"
|
||||||
|
borderTop="4px solid"
|
||||||
|
borderTopColor="teal.400"
|
||||||
|
width="full"
|
||||||
|
bg={useColorModeValue('white', 'gray.800')}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<chakra.div height="4.5rem" mx="auto" maxW="1200px">
|
||||||
|
<HeaderContent />
|
||||||
|
</chakra.div>
|
||||||
|
</chakra.header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostNav
|
||||||
|
|
@ -1,26 +1,136 @@
|
|||||||
import { chakra } from "@chakra-ui/react"
|
import { Box, chakra, Divider, Flex, Heading, HStack, IconButton, Image, VStack } from "@chakra-ui/react"
|
||||||
import Container from "components/container"
|
import Container from "components/container"
|
||||||
|
import LikeButton from "components/like-button"
|
||||||
|
import { MarkdownRender } from "components/markdown-editor/render"
|
||||||
|
import PostAuthor from "components/posts/post-author"
|
||||||
import SEO from "components/seo"
|
import SEO from "components/seo"
|
||||||
import siteConfig from "configs/site-config"
|
import siteConfig from "configs/site-config"
|
||||||
import Nav from "layouts/nav/nav"
|
import Nav from "layouts/nav/nav"
|
||||||
|
import PostNav from "layouts/nav/post-nav"
|
||||||
import PageContainer from "layouts/page-container"
|
import PageContainer from "layouts/page-container"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
import { useRouter } from "next/router"
|
import { useRouter } from "next/router"
|
||||||
import React from "react"
|
import { title } from "process"
|
||||||
|
import React, { useEffect, useState } from "react"
|
||||||
|
import { FaBookmark, FaGithub, FaRegBookmark, FaShare, FaShareAlt } from "react-icons/fa"
|
||||||
|
import { Post } from "src/types/posts"
|
||||||
|
import { requestApi } from "utils/axios/request"
|
||||||
|
|
||||||
const UserPage = () => {
|
const PostPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const slug = router.query.post_slug
|
||||||
|
const [post, setPost]: [Post, any] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slug) {
|
||||||
|
requestApi.get(`/post/${slug}`).then(res => setPost(res.data))
|
||||||
|
}
|
||||||
|
}, [slug])
|
||||||
|
|
||||||
|
const onLike = async () => {
|
||||||
|
await requestApi.post(`/post/like/${post.id}`)
|
||||||
|
const p = cloneDeep(post)
|
||||||
|
|
||||||
|
if (post.liked) {
|
||||||
|
p.likes += -1
|
||||||
|
p.liked = false
|
||||||
|
} else {
|
||||||
|
p.likes += 1
|
||||||
|
p.liked = true
|
||||||
|
}
|
||||||
|
setPost(p)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO
|
<SEO
|
||||||
title={siteConfig.seo.title}
|
title={siteConfig.seo.title}
|
||||||
description={siteConfig.seo.description}
|
description={siteConfig.seo.description}
|
||||||
/>
|
/>
|
||||||
<Nav />
|
<PageContainer nav={<PostNav />} mt="2rem">
|
||||||
<PageContainer>
|
{post &&
|
||||||
<chakra.h1>{router.query.username}的博文{router.query.post_slug}</chakra.h1>
|
<>
|
||||||
</PageContainer>
|
<HStack alignItems="top" spacing={[0, 0, 14, 14]}>
|
||||||
</>
|
<Box width={["100%", "100%", "75%", "75%"]} height="fit-content">
|
||||||
)}
|
<Image src={post.cover} />
|
||||||
|
<Box px="2">
|
||||||
export default UserPage
|
<Heading size="lg" my="6">{post.title}</Heading>
|
||||||
|
|
||||||
|
<Divider my="4" />
|
||||||
|
<PostAuthor post={post} />
|
||||||
|
<Divider my="4" />
|
||||||
|
|
||||||
|
<MarkdownRender md={post.md} py="2" mt="6" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<VStack alignItems="left" pos="fixed" display={{ base: "none", md: 'flex' }} width={["100%", "100%", "25%", "25%"]}>
|
||||||
|
<Box pt="16">
|
||||||
|
{/* <HStack mt="16"> */}
|
||||||
|
{/* <LikeButton type="like" count={post.likes} onClick={onLike} /> */}
|
||||||
|
<LikeButton type="unicorn" count={post.likes} onClick={onLike} liked={post.liked}/>
|
||||||
|
{/* </HStack> */}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<IconButton
|
||||||
|
mt="6"
|
||||||
|
aria-label="go to github"
|
||||||
|
variant="ghost"
|
||||||
|
layerStyle="textSecondary"
|
||||||
|
_focus={null}
|
||||||
|
fontSize="1.7rem"
|
||||||
|
fontWeight="300"
|
||||||
|
icon={<svg height="1.7rem" fill="currentColor" viewBox="0 0 384 512"><path d="M336 0H48C21.49 0 0 21.49 0 48v464l192-112 192 112V48c0-26.51-21.49-48-48-48zm16 456.287l-160-93.333-160 93.333V48c0-8.822 7.178-16 16-16h288c8.822 0 16 7.178 16 16v408.287z"></path></svg>}
|
||||||
|
/>
|
||||||
|
<Box mt="4">
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to github"
|
||||||
|
variant="ghost"
|
||||||
|
layerStyle="textSecondary"
|
||||||
|
_focus={null}
|
||||||
|
fontWeight="300"
|
||||||
|
icon={<svg height="1.7rem" fill="currentColor" viewBox="0 0 448 512"><path d="M352 320c-28.6 0-54.2 12.5-71.8 32.3l-95.5-59.7c9.6-23.4 9.7-49.8 0-73.2l95.5-59.7c17.6 19.8 43.2 32.3 71.8 32.3 53 0 96-43 96-96S405 0 352 0s-96 43-96 96c0 13 2.6 25.3 7.2 36.6l-95.5 59.7C150.2 172.5 124.6 160 96 160c-53 0-96 43-96 96s43 96 96 96c28.6 0 54.2-12.5 71.8-32.3l95.5 59.7c-4.7 11.3-7.2 23.6-7.2 36.6 0 53 43 96 96 96s96-43 96-96c-.1-53-43.1-96-96.1-96zm0-288c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zM96 320c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64zm256 160c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"></path></svg>}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack display={{ base: "flex", md: 'none' }} spacing="4" justifyContent="center">
|
||||||
|
<Box>
|
||||||
|
{/* <LikeButton type="like" count={post.likes} onClick={onLike}/> */}
|
||||||
|
<LikeButton type="unicorn" count={post.likes} onClick={onLike} liked={post.liked}/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to github"
|
||||||
|
variant="ghost"
|
||||||
|
layerStyle="textSecondary"
|
||||||
|
_focus={null}
|
||||||
|
fontSize="1.7rem"
|
||||||
|
fontWeight="300"
|
||||||
|
icon={<svg height="1.8rem" fill="currentColor" viewBox="0 0 384 512"><path d="M336 0H48C21.49 0 0 21.49 0 48v464l192-112 192 112V48c0-26.51-21.49-48-48-48zm16 456.287l-160-93.333-160 93.333V48c0-8.822 7.178-16 16-16h288c8.822 0 16 7.178 16 16v408.287z"></path></svg>}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to github"
|
||||||
|
variant="ghost"
|
||||||
|
layerStyle="textSecondary"
|
||||||
|
_focus={null}
|
||||||
|
fontWeight="300"
|
||||||
|
icon={<svg height="1.8rem" fill="currentColor" viewBox="0 0 448 512"><path d="M352 320c-28.6 0-54.2 12.5-71.8 32.3l-95.5-59.7c9.6-23.4 9.7-49.8 0-73.2l95.5-59.7c17.6 19.8 43.2 32.3 71.8 32.3 53 0 96-43 96-96S405 0 352 0s-96 43-96 96c0 13 2.6 25.3 7.2 36.6l-95.5 59.7C150.2 172.5 124.6 160 96 160c-53 0-96 43-96 96s43 96 96 96c28.6 0 54.2-12.5 71.8-32.3l95.5 59.7c-4.7 11.3-7.2 23.6-7.2 36.6 0 53 43 96 96 96s96-43 96-96c-.1-53-43.1-96-96.1-96zm0-288c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zM96 320c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64zm256 160c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"></path></svg>}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</PageContainer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostPage
|
||||||
|
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import { chakra } from "@chakra-ui/react"
|
|
||||||
import Container from "components/container"
|
|
||||||
import SEO from "components/seo"
|
|
||||||
import siteConfig from "configs/site-config"
|
|
||||||
import Nav from "layouts/nav/nav"
|
|
||||||
import PageContainer from "layouts/page-container"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
const PostPage = () => (
|
|
||||||
<>
|
|
||||||
<SEO
|
|
||||||
title={siteConfig.seo.title}
|
|
||||||
description={siteConfig.seo.description}
|
|
||||||
/>
|
|
||||||
<Nav />
|
|
||||||
<PageContainer>
|
|
||||||
<chakra.h1>Post</chakra.h1>
|
|
||||||
</PageContainer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default PostPage
|
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
package api
|
|
@ -0,0 +1,49 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/imdotdev/im.dev/server/internal/posts"
|
||||||
|
"github.com/imdotdev/im.dev/server/internal/session"
|
||||||
|
"github.com/imdotdev/im.dev/server/pkg/common"
|
||||||
|
"github.com/imdotdev/im.dev/server/pkg/e"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPost(c *gin.Context) {
|
||||||
|
slug := c.Param("slug")
|
||||||
|
|
||||||
|
ar, err := posts.GetPost(0, slug)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(err.Status, common.RespError(err.Message))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := session.CurrentUser(c)
|
||||||
|
if user == nil {
|
||||||
|
ar.Liked = false
|
||||||
|
} else {
|
||||||
|
ar.Liked = posts.GetLiked(ar.ID, user.ID)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.RespSuccess(ar))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LikePost(c *gin.Context) {
|
||||||
|
user := session.CurrentUser(c)
|
||||||
|
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if id == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := posts.Like(id, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(err.Status, common.RespError(err.Message))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.RespSuccess(nil))
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package posts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imdotdev/im.dev/server/pkg/db"
|
||||||
|
"github.com/imdotdev/im.dev/server/pkg/e"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Like(postId int64, userId int64) *e.Error {
|
||||||
|
// 判断文章是否存在
|
||||||
|
exist := postExist(postId)
|
||||||
|
if !exist {
|
||||||
|
return e.New(http.StatusNotFound, e.NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询当前like状态
|
||||||
|
liked := GetLiked(postId, userId)
|
||||||
|
|
||||||
|
if liked {
|
||||||
|
// 已经喜欢过该篇文章,更改为不喜欢
|
||||||
|
_, err := db.Conn.Exec("DELETE FROM post_like WHERE post_id=? and user_id=?", postId, userId)
|
||||||
|
if err != nil {
|
||||||
|
return e.New(http.StatusInternalServerError, e.Internal)
|
||||||
|
}
|
||||||
|
db.Conn.Exec("UPDATE posts SET like_count=like_count-1 WHERE id=?", postId)
|
||||||
|
} else {
|
||||||
|
_, err := db.Conn.Exec("INSERT INTO post_like (post_id,user_id) VALUES (?,?)", postId, userId)
|
||||||
|
if err != nil {
|
||||||
|
return e.New(http.StatusInternalServerError, e.Internal)
|
||||||
|
}
|
||||||
|
db.Conn.Exec("UPDATE posts SET like_count=like_count+1 WHERE id=?", postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLiked(postID, userID int64) bool {
|
||||||
|
liked := false
|
||||||
|
var nid int64
|
||||||
|
err := db.Conn.QueryRow("SELECT post_id FROM post_like WHERE post_id=? and user_id=?", postID, userID).Scan(&nid)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
logger.Warn("query post like error", "error", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if nid != 0 {
|
||||||
|
liked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return liked
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import React from "react"
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
|
Image,
|
||||||
|
Button
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import useSession from "hooks/use-session"
|
||||||
|
import { Session } from "src/types/session"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import storage from "utils/localStorage"
|
||||||
|
import { ReserveUrls } from "src/data/reserve-urls"
|
||||||
|
import { FaRegSun, FaUserAlt ,FaBookmark, FaSignOutAlt,FaEdit,FaStar} from "react-icons/fa"
|
||||||
|
import { isAdmin, isEditor } from "utils/role"
|
||||||
|
import { logout } from "utils/session"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
export const AccountMenu = () => {
|
||||||
|
const session: Session = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
console.log(router)
|
||||||
|
storage.set("current-page", router.asPath)
|
||||||
|
router.push(ReserveUrls.Login)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{session ?
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
bg="transparent"
|
||||||
|
_focus={null}
|
||||||
|
icon={session.user.avatar !== '' ? <Image
|
||||||
|
boxSize="2.8em"
|
||||||
|
borderRadius="full"
|
||||||
|
src="https://placekitten.com/100/100"
|
||||||
|
alt="user"
|
||||||
|
/> :
|
||||||
|
<FaUserAlt />
|
||||||
|
}
|
||||||
|
aria-label="Options"
|
||||||
|
ml={{ base: "0", md: "2" }}
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem icon={<FaUserAlt fontSize="16" />}>
|
||||||
|
<span>Sunface</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
{isEditor(session.user.role) && <Link href={`${ReserveUrls.Editor}/posts`}><MenuItem icon={<FaEdit fontSize="16" />} >创作中心</MenuItem></Link>}
|
||||||
|
{isAdmin(session.user.role) && <Link href={`${ReserveUrls.Admin}/tags`}><MenuItem icon={<FaStar fontSize="16" />} >管理员</MenuItem></Link>}
|
||||||
|
<MenuItem icon={<FaBookmark fontSize="16" />}>书签收藏</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem icon={<FaRegSun fontSize="16" />}>偏好设置</MenuItem>
|
||||||
|
<MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}>账号登出</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu> :
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
ml="2"
|
||||||
|
colorScheme="teal"
|
||||||
|
fontSize=".8rem"
|
||||||
|
onClick={() => login()}
|
||||||
|
>
|
||||||
|
SIGN IN
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountMenu
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { IconButton, useColorMode, useColorModeValue } from "@chakra-ui/react"
|
||||||
|
import { FaMoon, FaSun } from "react-icons/fa"
|
||||||
|
|
||||||
|
export const DarkMode = () => {
|
||||||
|
const { toggleColorMode: toggleMode } = useColorMode()
|
||||||
|
const text = useColorModeValue("dark", "light")
|
||||||
|
const SwitchIcon = useColorModeValue(FaMoon, FaSun)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
fontSize="lg"
|
||||||
|
aria-label={`Switch to ${text} mode`}
|
||||||
|
variant="ghost"
|
||||||
|
color="current"
|
||||||
|
onClick={toggleMode}
|
||||||
|
_focus={null}
|
||||||
|
icon={<SwitchIcon />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DarkMode
|
@ -0,0 +1,41 @@
|
|||||||
|
import { chakra, HStack, IconButton, Image, Tooltip, useColorMode, useColorModeValue } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: string
|
||||||
|
count: number
|
||||||
|
onClick: any
|
||||||
|
liked: boolean
|
||||||
|
}
|
||||||
|
const LikeButton = (props: Props) => {
|
||||||
|
let imgSrc: string
|
||||||
|
let label: string
|
||||||
|
switch (props.type) {
|
||||||
|
case "like":
|
||||||
|
imgSrc = "https://cdn.hashnode.com/res/hashnode/image/upload/v1594643814744/9iXxz71TL.png?auto=compress"
|
||||||
|
label = "Love it"
|
||||||
|
break;
|
||||||
|
case "unicorn":
|
||||||
|
imgSrc = "https://cdn.hashnode.com/res/hashnode/image/upload/v1594643772437/FYDU5k2kQ.png?auto=compress"
|
||||||
|
label = "I love it"
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<HStack>
|
||||||
|
<Tooltip label={label} size="sm">
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to github"
|
||||||
|
variant="ghost"
|
||||||
|
color="current"
|
||||||
|
_focus={null}
|
||||||
|
icon={<Image width="38px" src={imgSrc} />}
|
||||||
|
onClick={props.onClick}
|
||||||
|
border={props.liked ? `1px solid ${useColorModeValue('gray','pink')}` : null}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<chakra.span layerStyle="textSecondary" fontWeight="600" marginBottom="-3px">{props.count}</chakra.span>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LikeButton
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react"
|
||||||
|
import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack} from "@chakra-ui/react"
|
||||||
|
import { Tag } from "src/types/tag"
|
||||||
|
import { ReserveUrls } from "src/data/reserve-urls"
|
||||||
|
import NextLink from "next/link"
|
||||||
|
import { Post } from "src/types/posts"
|
||||||
|
import moment from 'moment'
|
||||||
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
type Props = PropsOf<typeof chakra.div> & {
|
||||||
|
post : Post
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PostAuthor= ({post}:Props) =>{
|
||||||
|
const router = useRouter()
|
||||||
|
console.log(post)
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="space-between">
|
||||||
|
<HStack spacing="4">
|
||||||
|
<Avatar src={post.creator.avatar} size="lg" onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer"/>
|
||||||
|
<VStack alignItems="left" spacing="1">
|
||||||
|
<Heading size="sm" onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer">{post.creator.nickname === "" ? post.creator.username : post.creator.nickname}</Heading>
|
||||||
|
<Text layerStyle="textSecondary" fontSize=".9rem">发布于<chakra.span fontWeight="600" ml="1">{moment(post.created).fromNow()}</chakra.span></Text>
|
||||||
|
<HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3">
|
||||||
|
<FaGithub /> <chakra.span>4 min read</chakra.span>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostAuthor
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react"
|
||||||
|
import {Box, Heading, Image, Text, HStack,Button, Flex,PropsOf,Link} from "@chakra-ui/react"
|
||||||
|
import { Tag } from "src/types/tag"
|
||||||
|
import { ReserveUrls } from "src/data/reserve-urls"
|
||||||
|
import NextLink from "next/link"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tag: Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const TagCard= (props:Props) =>{
|
||||||
|
const {tag} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="space-between">
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" display="flex" alignItems="center" cursor="pointer">
|
||||||
|
{tag.title}
|
||||||
|
</Heading>
|
||||||
|
<Text layerStyle="textSecondary" fontSize=".9rem" mt="1" fontWeight="450">{tag.postCount} posts</Text>
|
||||||
|
</Box>
|
||||||
|
<Image src={tag.icon} width="35px" />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagCard
|
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useEffect, useState } from "react"
|
||||||
|
import { Box, Popover, PopoverTrigger, Button, PopoverContent, PopoverBody, Input, useDisclosure, Divider, useToast } from "@chakra-ui/react"
|
||||||
|
import { Tag } from "src/types/tag"
|
||||||
|
import { requestApi } from "utils/axios/request"
|
||||||
|
import { cloneDeep, findIndex } from "lodash"
|
||||||
|
import TagCard from 'src/components/posts/tag-list-card'
|
||||||
|
import { config } from "utils/config"
|
||||||
|
interface Props {
|
||||||
|
options: Tag[]
|
||||||
|
selected: Tag[]
|
||||||
|
onChange: any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const TagInput = (props: Props) => {
|
||||||
|
const toast = useToast()
|
||||||
|
const [tags, setTags]: [Tag[], any] = useState([])
|
||||||
|
|
||||||
|
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||||
|
|
||||||
|
const filterTags = query => {
|
||||||
|
if (query.trim() === "") {
|
||||||
|
setTags([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTags = []
|
||||||
|
props.options.forEach(tag => {
|
||||||
|
if (tag.title.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
|
||||||
|
if (findIndex(props.selected,t => t.id === tag.id) === -1) {
|
||||||
|
newTags.push(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setTags(newTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTag = tag => {
|
||||||
|
const t = cloneDeep(props.selected)
|
||||||
|
t.push(tag)
|
||||||
|
props.onChange(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.selected.length <=config.posts.maxTags && <Input onChange={e => filterTags(e.target.value)} onFocus={onOpen} onBlur={onClose} placeholder="start typing to search.." variant="unstyled" _focus={null} mt="3" />}
|
||||||
|
{tags.length > 0 && <Popover isOpen={isOpen} closeOnBlur={false} placement="bottom-start" onOpen={onOpen} onClose={onClose} autoFocus={false}>
|
||||||
|
<PopoverTrigger><Box width="100%"></Box></PopoverTrigger>
|
||||||
|
<PopoverContent width="100%">
|
||||||
|
<PopoverBody width="100%" p="0">
|
||||||
|
{tags.map((tag, i) => {
|
||||||
|
return <Box key={tag.id} cursor="pointer" onClick={_ => addTag(tag)}>
|
||||||
|
<Box py="2" px="4" >
|
||||||
|
<TagCard tag={tag}/>
|
||||||
|
</Box>
|
||||||
|
{i < tags.length - 1 && <Divider />}
|
||||||
|
</Box>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagInput
|
@ -0,0 +1,62 @@
|
|||||||
|
import { mode } from "@chakra-ui/theme-tools"
|
||||||
|
import userCustomTheme from "./user-custom"
|
||||||
|
|
||||||
|
export default function markdownRender(props) {
|
||||||
|
return {
|
||||||
|
'.markdown-render': {
|
||||||
|
'.hljs' : {
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px'
|
||||||
|
},
|
||||||
|
'ul,ol' : {
|
||||||
|
paddingLeft: '1rem',
|
||||||
|
margin: '1.2rem 0',
|
||||||
|
li: {
|
||||||
|
margin: '.8rem 0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'h1': {
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '0.8rem'
|
||||||
|
},
|
||||||
|
'h2': {
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '0.6rem'
|
||||||
|
},
|
||||||
|
'h3': {
|
||||||
|
fontSize: '1.6em',
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: '0.4rem'
|
||||||
|
},
|
||||||
|
'h4': {
|
||||||
|
fontSize: '1.4em',
|
||||||
|
fontWeight: '600'
|
||||||
|
},
|
||||||
|
'h5,h6': {
|
||||||
|
fontSize: '1.2em',
|
||||||
|
fontWeight: 'normal'
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
margin: '1.2rem 0',
|
||||||
|
},
|
||||||
|
blockquote: {
|
||||||
|
lineHeight: '2rem',
|
||||||
|
margin: '1.5rem 0',
|
||||||
|
p :{
|
||||||
|
paddingLeft: '1rem',
|
||||||
|
fontWeight: '500',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
borderLeftWidth: '.25rem',
|
||||||
|
borderLeftColor: '#e5e7eb',
|
||||||
|
color: mode("inherit", "'rgb(189, 189, 189)'")(props),
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pre: {
|
||||||
|
margin: '1.6rem 0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue