pull/50/head
sunface 4 years ago
parent 74b02b029e
commit c5205e7e98

@ -2,8 +2,6 @@ import {
chakra, chakra,
Flex, Flex,
Button, Button,
IconButton,
useColorMode,
useColorModeValue, useColorModeValue,
Box, Box,
useRadioGroup, useRadioGroup,
@ -14,26 +12,50 @@ import {
DrawerOverlay, DrawerOverlay,
DrawerContent, DrawerContent,
Divider, Divider,
Heading Heading,
Tag as ChakraTag,
TagLabel,
TagCloseButton
} from "@chakra-ui/react" } from "@chakra-ui/react"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
import React from "react" import React, { useEffect, useState } from "react"
import { FaMoon, FaSun } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo" import Logo, { LogoIcon } from "src/components/logo"
import RadioCard from "components/radio-card" import RadioCard from "components/radio-card"
import { EditMode } from "src/types/editor" import { EditMode } from "src/types/editor"
import Card from "components/card" import Card from "components/card"
import TagInput from "components/tag-input"
import { Tag } from "src/types/tag"
import { cloneDeep, remove } from "lodash"
import { requestApi } from "utils/axios/request"
import DarkMode from "components/dark-mode"
function HeaderContent(props: any) { function HeaderContent(props: any) {
const { toggleColorMode: toggleMode } = useColorMode() const [tags,setTags]:[Tag[],any] = useState([])
const text = useColorModeValue("dark", "light") const [allTags,setAllTags] = useState([])
const SwitchIcon = useColorModeValue(FaMoon, FaSun)
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
useEffect(() => {
requestApi.get('/tags').then(res => {
setAllTags(res.data)
const t = []
props.ar.tags?.forEach(id => {
res.data.forEach(tag => {
if (tag.id === id) {
t.push(tag)
}
})
})
setTags(t)
})
},[props.ar])
const editOptions = [EditMode.Edit, EditMode.Preview] const editOptions = [EditMode.Edit, EditMode.Preview]
const { getRootProps, getRadioProps } = useRadioGroup({ const { getRootProps, getRadioProps } = useRadioGroup({
name: "framework", name: "framework",
@ -43,7 +65,24 @@ function HeaderContent(props: any) {
}, },
}) })
const group = getRootProps() const group = getRootProps()
const addTag = t => {
setTags(t)
const ids = []
t.forEach(tag => ids.push(tag.id))
props.ar.tags = ids
}
const removeTag = t => {
const newTags = cloneDeep(tags)
remove(newTags, tag => tag.id === t.id)
setTags(newTags)
const ids = []
newTags.forEach(tag => ids.push(tag.id))
props.ar.tags = ids
}
return ( return (
<> <>
@ -76,17 +115,7 @@ function HeaderContent(props: any) {
<Box <Box
color={useColorModeValue("gray.500", "gray.400")} color={useColorModeValue("gray.500", "gray.400")}
> >
<IconButton <DarkMode />
size="md"
fontSize="lg"
aria-label={`Switch to ${text} mode`}
variant="ghost"
color="current"
ml={{ base: "0", md: "1" }}
onClick={toggleMode}
_focus={null}
icon={<SwitchIcon />}
/>
<Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button> <Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button>
</Box> </Box>
</Flex> </Flex>
@ -109,7 +138,24 @@ function HeaderContent(props: any) {
<Heading size="xs"> <Heading size="xs">
</Heading> </Heading>
<Input value={props.ar.cover} onChange={(e) => {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接你可以用github当图片存储服务" focusBorderColor="teal.400"/> <Input value={props.ar.cover} onChange={(e) => {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="unstyled" size="sm" placeholder="输入链接可以用github或postimg.cc当图片存储服务.." focusBorderColor="teal.400"/>
</Card>
<Card mt="4">
<Heading size="xs">
</Heading>
<TagInput options={allTags} selected={tags} onChange={addTag}/>
{tags.length > 0&& <Box mt="2">
{
tags.map(tag =>
<ChakraTag key={tag.id} mr="2" colorScheme="teal" variant="solid" px="2" py="1">
<TagLabel>{tag.title}</TagLabel>
<TagCloseButton onClick={ _ => removeTag(tag)}/>
</ChakraTag>)
}
</Box>}
</Card> </Card>
</DrawerContent> </DrawerContent>
</DrawerOverlay> </DrawerOverlay>
@ -119,7 +165,6 @@ function HeaderContent(props: any) {
} }
function EditorNav(props) { function EditorNav(props) {
const bg = useColorModeValue("white", "gray.800")
const ref = React.useRef<HTMLHeadingElement>() const ref = React.useRef<HTMLHeadingElement>()
const [y, setY] = React.useState(0) const [y, setY] = React.useState(0)
const { height = 0 } = ref.current?.getBoundingClientRect() ?? {} const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
@ -137,7 +182,7 @@ function EditorNav(props) {
pos="fixed" pos="fixed"
top="0" top="0"
zIndex="3" zIndex="3"
// bg={bg} bg={useColorModeValue('white','gray.800')}
left="0" left="0"
right="0" right="0"
borderTop="4px solid" borderTop="4px solid"

@ -1,37 +1,26 @@
import { import {
chakra, chakra,
Flex, Flex,
Button,
HStack, HStack,
IconButton, IconButton,
useColorMode,
useColorModeValue, useColorModeValue,
useDisclosure, useDisclosure,
useUpdateEffect, useUpdateEffect,
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
Image,
Box Box
} from "@chakra-ui/react" } from "@chakra-ui/react"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
import React from "react" import React from "react"
import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt,FaStar, FaGithub, FaBookmark, FaEdit } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo" import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "../mobile-nav" import { MobileNavButton, MobileNavContent } from "./mobile-nav"
import AlgoliaSearch from "src/components/search/algolia-search" import AlgoliaSearch from "src/components/search/algolia-search"
import useSession from "hooks/use-session"
import { Session } from "src/types/session"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import storage from "utils/localStorage"
import { logout } from "utils/session"
import { isAdmin, isEditor } from "utils/role"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import Link from "next/link" import Link from "next/link"
import DarkMode from "components/dark-mode"
import AccountMenu from "components/account-menu"
const navLinks = [{ const navLinks = [{
title: '主页', title: '主页',
@ -53,22 +42,13 @@ function HeaderContent() {
const { asPath } = router const { asPath } = router
const mobileNav = useDisclosure() const mobileNav = useDisclosure()
const session: Session = useSession()
const { toggleColorMode: toggleMode } = useColorMode()
const text = useColorModeValue("dark", "light")
const SwitchIcon = useColorModeValue(FaMoon, FaSun)
const mobileNavBtnRef = React.useRef<HTMLButtonElement>() const mobileNavBtnRef = React.useRef<HTMLButtonElement>()
useUpdateEffect(() => { useUpdateEffect(() => {
mobileNavBtnRef.current?.focus() mobileNavBtnRef.current?.focus()
}, [mobileNav.isOpen]) }, [mobileNav.isOpen])
const login = () => {
console.log(router)
storage.set("current-page", asPath)
router.push(ReserveUrls.Login)
}
return ( return (
<> <>
@ -86,92 +66,39 @@ function HeaderContent() {
</NextLink> </NextLink>
<HStack display={{ base: "none", md: "flex" }} ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" minWidth="250px"> <HStack display={{ base: "none", md: "flex" }} ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" minWidth="250px">
{navLinks.map(link => <Box px="4" py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} ><Link href={link.url}>{link.title}</Link></Box>)} {navLinks.map(link => <Box px="4" py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} ><Link href={link.url}>{link.title}</Link></Box>)}
</HStack> </HStack>
</Flex> </Flex>
<Flex <HStack
w="100%" w="100%"
maxW="600px" maxW="600px"
align="center" align="center"
color={useColorModeValue("gray.500","gray.400")} color={useColorModeValue("gray.500", "gray.400")}
> >
<AlgoliaSearch /> <AlgoliaSearch />
<HStack spacing="5" display={{ base: "none", md: "flex" }}> <Link
<Link aria-label="Go to Chakra UI GitHub page"
aria-label="Go to Chakra UI GitHub page" href={siteConfig.repo.url}
href={siteConfig.repo.url} >
> <IconButton
<IconButton size="md"
size="md" fontSize="lg"
fontSize="lg" aria-label="go to github"
aria-label={`Switch to ${text} mode`} variant="ghost"
variant="ghost" color="current"
color="current" _focus={null}
ml={{ base: "0", md: "3" }} icon={<FaGithub />}
_focus={null} />
icon={<FaGithub />} </Link>
/> <DarkMode />
</Link> <AccountMenu />
</HStack>
<IconButton
size="md"
fontSize="lg"
aria-label={`Switch to ${text} mode`}
variant="ghost"
color="current"
ml={{ base: "0", md: "1" }}
onClick={toggleMode}
_focus={null}
icon={<SwitchIcon />}
/>
{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()}
// leftIcon={<FaUserAlt />}
>
SIGN IN
</Button>
}
<MobileNavButton <MobileNavButton
ref={mobileNavBtnRef} ref={mobileNavBtnRef}
aria-label="Open Menu" aria-label="Open Menu"
onClick={mobileNav.onOpen} onClick={mobileNav.onOpen}
/> />
</Flex> </HStack>
</Flex> </Flex>
<MobileNavContent isOpen={mobileNav.isOpen} onClose={mobileNav.onClose} /> <MobileNavContent isOpen={mobileNav.isOpen} onClose={mobileNav.onClose} />
</> </>
@ -201,6 +128,7 @@ function Header(props) {
borderTop="4px solid" borderTop="4px solid"
borderTopColor="teal.400" borderTopColor="teal.400"
width="full" width="full"
bg={useColorModeValue('white', 'gray.800')}
{...props} {...props}
> >
<chakra.div height="4.5rem" mx="auto" maxW="1200px"> <chakra.div height="4.5rem" mx="auto" maxW="1200px">

@ -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,4 +1,4 @@
import { Badge, Box, chakra } from "@chakra-ui/react" import { Badge, Box, chakra,PropsOf } from "@chakra-ui/react"
import { SkipNavContent, SkipNavLink } from "@chakra-ui/skip-nav" import { SkipNavContent, SkipNavLink } from "@chakra-ui/skip-nav"
import Container from "components/container" import Container from "components/container"
import Footer from "./footer" import Footer from "./footer"
@ -24,13 +24,14 @@ function useHeadingFocusOnRouteChange() {
}, []) }, [])
} }
interface PageContainerProps { type PageContainerProps = PropsOf<typeof chakra.div> & {
children: React.ReactNode children: React.ReactNode
nav?: any nav?: any
} }
function PageContainer(props: PageContainerProps) { function PageContainer(props: PageContainerProps) {
const { children ,nav} = props const { children ,nav, ...rest} = props
useHeadingFocusOnRouteChange() useHeadingFocusOnRouteChange()
return ( return (
@ -48,9 +49,10 @@ function PageContainer(props: PageContainerProps) {
<Box <Box
id="content" id="content"
pt={3} pt={3}
px={{base:0,md:3}} px={[0,0,2,3]}
mt="4.5rem" mt={props.mt ?? "4.5rem"}
mx="auto" mx="auto"
{...rest}
> >
<PageTransition> <PageTransition>
{children} {children}

@ -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

@ -15,8 +15,7 @@ const UserPage = () => {
title={siteConfig.seo.title} title={siteConfig.seo.title}
description={siteConfig.seo.description} description={siteConfig.seo.description}
/> />
<Nav /> <PageContainer mt="6rem">
<PageContainer>
<chakra.h1>{router.query.username}'s home</chakra.h1> <chakra.h1>{router.query.username}'s home</chakra.h1>
</PageContainer> </PageContainer>
</> </>

@ -6,7 +6,7 @@ import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import {adminLinks} from "src/data/links" import {adminLinks} from "src/data/links"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import TagCard from "components/posts/tag-card" import TagCard from "components/posts/tag-edit-card"
import { Post } from "src/types/posts" import { Post } from "src/types/posts"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import Link from "next/link" import Link from "next/link"
@ -44,7 +44,6 @@ const PostsPage = () => {
return ( return (
<> <>
<Nav />
<PageContainer> <PageContainer>
<Box display="flex"> <Box display="flex">
<Sidebar routes={adminLinks} width="250px" height="fit-content" /> <Sidebar routes={adminLinks} width="250px" height="fit-content" />

@ -1,4 +1,4 @@
import { Box, Button, useToast} from '@chakra-ui/react'; import { Box, Button, useToast } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { MarkdownEditor } from 'components/markdown-editor/editor'; import { MarkdownEditor } from 'components/markdown-editor/editor';
import PageContainer from 'layouts/page-container'; import PageContainer from 'layouts/page-container';
@ -18,19 +18,19 @@ const content = `
function PostEditPage() { function PostEditPage() {
const router = useRouter() const router = useRouter()
const {id} = router.query const { id } = router.query
const [editMode, setEditMode] = useState(EditMode.Edit) const [editMode, setEditMode] = useState(EditMode.Edit)
const [ar,setAr] = useState({ const [ar, setAr] = useState({
md: content, md: content,
title: '' title: ''
}) })
const toast = useToast() const toast = useToast()
useEffect(() => { useEffect(() => {
if (id && id !== 'new') { if (id && id !== 'new') {
requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data)) requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data))
} }
},[id]) }, [id])
const onMdChange = newMd => { const onMdChange = newMd => {
setAr({ setAr({
@ -38,7 +38,7 @@ function PostEditPage() {
md: newMd md: newMd
}) })
} }
const onChange = () => { const onChange = () => {
setAr(cloneDeep(ar)) setAr(cloneDeep(ar))
} }
@ -50,20 +50,20 @@ function PostEditPage() {
status: "error", status: "error",
duration: 2000, duration: 2000,
isClosable: true, isClosable: true,
}) })
return return
} }
setAr({...ar, title: title}) setAr({ ...ar, title: title })
} }
const publish = async () => { const publish = async () => {
const res = await requestApi.post(`/editor/post`, ar) const res = await requestApi.post(`/editor/post`, ar)
toast({ toast({
description: "发布成功", description: "发布成功",
status: "success", status: "success",
duration: 2000, duration: 2000,
isClosable: true, isClosable: true,
}) })
router.push(`/${res.data.username}/${res.data.slug}`) router.push(`/${res.data.username}/${res.data.slug}`)
} }
@ -78,17 +78,18 @@ function PostEditPage() {
publish={() => publish()} publish={() => publish()}
/>} />}
> >
<Card style={{ height: 'calc(100vh - 145px)' }}> {editMode === EditMode.Edit ?
{editMode === EditMode.Edit ? <Card style={{ height: 'calc(100vh - 145px)' }}>
<MarkdownEditor <MarkdownEditor
onChange={(md) => onMdChange(md)} onChange={(md) => onMdChange(md)}
md={ar.md} md={ar.md}
/> : /></Card> :
<Card>
<Box height="100%" p="6"> <Box height="100%" p="6">
<MarkdownRender md={ar.md} /> <MarkdownRender md={ar.md} />
</Box> </Box>
} </Card>
</Card> }
</PageContainer> </PageContainer>
); );
} }

@ -97,7 +97,6 @@ const PostsPage = () => {
return ( return (
<> <>
<Nav />
<PageContainer> <PageContainer>
<Box display="flex"> <Box display="flex">
<Sidebar routes={editorLinks} width="250px" height="fit-content" /> <Sidebar routes={editorLinks} width="250px" height="fit-content" />

@ -12,7 +12,6 @@ const HomePage = () => (
title={siteConfig.seo.title} title={siteConfig.seo.title}
description={siteConfig.seo.description} description={siteConfig.seo.description}
/> />
<Nav />
<PageContainer> <PageContainer>
<Card width="200px"> <Card width="200px">
<chakra.h1>NOT FOUND</chakra.h1> <chakra.h1>NOT FOUND</chakra.h1>

@ -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

@ -36,7 +36,6 @@ const UserPage = () => {
title={siteConfig.seo.title} title={siteConfig.seo.title}
description={siteConfig.seo.description} description={siteConfig.seo.description}
/> />
<Nav />
<PageContainer> <PageContainer>
{tag.name && <HStack alignItems="top" spacing="4"> {tag.name && <HStack alignItems="top" spacing="4">
<Box width="70%"> <Box width="70%">
@ -65,7 +64,7 @@ const UserPage = () => {
</Box> </Box>
<Box> <Box>
<Heading size="lg">13.4K</Heading> <Heading size="lg">{tag.postCount}</Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Posts</Text> <Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Posts</Text>
</Box> </Box>
</Flex> </Flex>

@ -1,7 +1,6 @@
package api package api
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -63,7 +62,6 @@ func DeletePost(c *gin.Context) {
func GetEditorPost(c *gin.Context) { func GetEditorPost(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64) id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
fmt.Println(c.Param("id"))
if id == 0 { if id == 0 {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return return
@ -81,7 +79,7 @@ func GetEditorPost(c *gin.Context) {
return return
} }
ar, err := posts.GetPost(id) ar, err := posts.GetPost(id, "")
if err != nil { if err != nil {
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return

@ -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
}

@ -97,14 +97,17 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
md := utils.Compress(post.Md) md := utils.Compress(post.Md)
setSlug(user.ID, post) setSlug(user.ID, post)
if post.ID == 0 { if post.ID == 0 {
//create //create
_, err = db.Conn.Exec("INSERT INTO posts (creator,slug, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?,?)", res, err := db.Conn.Exec("INSERT INTO posts (creator,slug, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?,?)",
user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, now) user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, now)
if err != nil { if err != nil {
logger.Warn("submit post error", "error", err) logger.Warn("submit post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
} }
post.ID, _ = res.LastInsertId()
} else { } else {
// 只有创建者自己才能更新内容 // 只有创建者自己才能更新内容
creator, _ := GetPostCreator(post.ID) creator, _ := GetPostCreator(post.ID)
@ -120,6 +123,23 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
} }
} }
//update tags
// "tag_post": `CREATE TABLE IF NOT EXISTS tag_post (
// tag_id INTEGER,
// post_id INTEGER
// );
_, err = db.Conn.Exec("DELETE FROM tag_post WHERE post_id=?", post.ID)
if err != nil {
logger.Warn("delete post tags error", "error", err)
}
for _, tag := range post.Tags {
_, err = db.Conn.Exec("INSERT INTO tag_post (tag_id,post_id) VALUES (?,?)", tag, post.ID)
if err != nil {
logger.Warn("add post tag error", "error", err)
}
}
return map[string]string{ return map[string]string{
"username": user.Username, "username": user.Username,
"slug": post.Slug, "slug": post.Slug,
@ -136,11 +156,11 @@ func DeletePost(id int64) *e.Error {
return nil return nil
} }
func GetPost(id int64) (*models.Post, *e.Error) { func GetPost(id int64, slug string) (*models.Post, *e.Error) {
ar := &models.Post{} ar := &models.Post{}
var rawmd []byte var rawmd []byte
err := db.Conn.QueryRow("select id,slug,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).Scan( err := db.Conn.QueryRow("select id,slug,title,md,url,cover,brief,creator,like_count,created,updated from posts where id=? or slug=?", id, slug).Scan(
&ar.ID, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated, &ar.ID, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Likes, &ar.Created, &ar.Updated,
) )
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -155,6 +175,19 @@ func GetPost(id int64) (*models.Post, *e.Error) {
ar.Creator = &models.UserSimple{ID: ar.CreatorID} ar.Creator = &models.UserSimple{ID: ar.CreatorID}
err = ar.Creator.Query() err = ar.Creator.Query()
// get tags
tags := make([]int64, 0)
rows, err := db.Conn.Query("SELECT tag_id FROM tag_post WHERE post_id=?", id)
if err != nil && err != sql.ErrNoRows {
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
var tag int64
err = rows.Scan(&tag)
tags = append(tags, tag)
}
ar.Tags = tags
return ar, nil return ar, nil
} }
@ -172,6 +205,21 @@ func GetPostCreator(id int64) (int64, *e.Error) {
return uid, nil return uid, nil
} }
func postExist(id int64) bool {
var nid int64
err := db.Conn.QueryRow("SELECT id from posts WHERE id=?", id).Scan(&nid)
if err != nil {
logger.Warn("query post error", "error", err)
return false
}
if nid == 0 {
return false
}
return true
}
//slug有三个规则 //slug有三个规则
// 1. 长度不能超过127 // 1. 长度不能超过127
// 2. 每次title更新都要重新生成slug // 2. 每次title更新都要重新生成slug
@ -189,7 +237,6 @@ func setSlug(creator int64, post *models.Post) error {
return err return err
} }
fmt.Println(count)
if count == 0 { if count == 0 {
post.Slug = slug post.Slug = slug
} else { } else {

@ -84,6 +84,8 @@ func GetTags() (models.Tags, *e.Error) {
} }
tags = append(tags, tag) tags = append(tags, tag)
db.Conn.QueryRow("SELECT count(*) FROM tag_post WHERE tag_id=?", tag.ID).Scan(&tag.PostCount)
} }
sort.Sort(tags) sort.Sort(tags)
@ -118,5 +120,7 @@ func GetTag(name string) (*models.Tag, *e.Error) {
md, _ := utils.Uncompress(rawmd) md, _ := utils.Uncompress(rawmd)
tag.Md = string(md) tag.Md = string(md)
db.Conn.QueryRow("SELECT count(*) FROM tag_post WHERE tag_id=?", tag.ID).Scan(&tag.PostCount)
return tag, nil return tag, nil
} }

@ -45,6 +45,13 @@ func (s *Server) Start() error {
r.POST("/login", session.Login) r.POST("/login", session.Login)
r.POST("/logout", session.Logout) r.POST("/logout", session.Logout)
r.GET("/uiconfig", GetUIConfig) r.GET("/uiconfig", GetUIConfig)
}
postR := r.Group("/post")
{
postR.GET("/:slug", api.GetPost)
postR.POST("/like/:id", api.LikePost, IsLogin())
} }
// login apis // login apis

@ -37,7 +37,7 @@ var sqlTables = map[string]string{
url VARCHAR(255), url VARCHAR(255),
cover VARCHAR(255), cover VARCHAR(255),
brief TEXT, brief TEXT,
like_count INTEGER DEFAULT 0,
created DATETIME NOT NULL, created DATETIME NOT NULL,
updated DATETIME updated DATETIME
); );
@ -49,6 +49,16 @@ var sqlTables = map[string]string{
ON posts (creator, slug); ON posts (creator, slug);
`, `,
"post_like": `CREATE TABLE IF NOT EXISTS post_like (
post_id INTEGER,
user_id INTEGER
);
CREATE INDEX IF NOT EXISTS post_like_postid
ON post_like (post_id);
CREATE INDEX IF NOT EXISTS post_like_userid
ON post_like (user_id);
`,
"tags": `CREATE TABLE IF NOT EXISTS tags ( "tags": `CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
creator INTEGER NOT NULL, creator INTEGER NOT NULL,
@ -57,7 +67,7 @@ var sqlTables = map[string]string{
icon VARCHAR(255), icon VARCHAR(255),
cover VARCHAR(255), cover VARCHAR(255),
md TEXT, md TEXT,
follower_count INTEGER DEFAULT 0,
created DATETIME NOT NULL, created DATETIME NOT NULL,
updated DATETIME updated DATETIME
); );
@ -66,4 +76,14 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS tags_created CREATE INDEX IF NOT EXISTS tags_created
ON tags (created); ON tags (created);
`, `,
"tag_post": `CREATE TABLE IF NOT EXISTS tag_post (
tag_id INTEGER,
post_id INTEGER
);
CREATE INDEX IF NOT EXISTS tag_post_tagid
ON tag_post (tag_id);
CREATE INDEX IF NOT EXISTS tag_post_postid
ON tag_post (post_id);
`,
} }

@ -9,21 +9,24 @@ import (
) )
type UIConfig struct { type UIConfig struct {
Posts *UIPosts `json:"posts"` Posts *PostsConfig `json:"posts"`
} }
type UIPosts struct { type PostsConfig struct {
TitleMaxLen int `json:"titleMaxLen"` TitleMaxLen int `json:"titleMaxLen"`
BriefMaxLen int `json:"briefMaxLen"` BriefMaxLen int `json:"briefMaxLen"`
WritingEnabled bool `json:"writingEnabled"` WritingEnabled bool `json:"writingEnabled"`
MaxTags int `json:"maxTags"`
} }
// 在后台页面配置存储到mysql中
func GetUIConfig(c *gin.Context) { func GetUIConfig(c *gin.Context) {
conf := &UIConfig{ conf := &UIConfig{
Posts: &UIPosts{ Posts: &PostsConfig{
TitleMaxLen: config.Data.Posts.TitleMaxLen, TitleMaxLen: config.Data.Posts.TitleMaxLen,
BriefMaxLen: config.Data.Posts.BriefMaxLen, BriefMaxLen: config.Data.Posts.BriefMaxLen,
WritingEnabled: config.Data.Posts.WritingEnabled, WritingEnabled: config.Data.Posts.WritingEnabled,
MaxTags: 2,
}, },
} }

@ -3,17 +3,21 @@ package models
import "time" import "time"
type Post struct { type Post struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Creator *UserSimple `json:"creator"` Creator *UserSimple `json:"creator"`
CreatorID int64 `json:"creatorId"` CreatorID int64 `json:"creatorId"`
Title string `json:"title"` Title string `json:"title"`
Slug string `json:"slug"` Slug string `json:"slug"`
Md string `json:"md"` Md string `json:"md"`
URL string `json:"url"` URL string `json:"url"`
Cover string `json:"cover"` Cover string `json:"cover"`
Brief string `json:"brief"` Brief string `json:"brief"`
Created time.Time `json:"created"` Tags []int64 `json:"tags"`
Updated time.Time `json:"updated"` Likes int `json:"likes"`
Liked bool `json:"liked"`
Recommands int `json:"recommands"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type Posts []*Post type Posts []*Post

@ -3,15 +3,16 @@ package models
import "time" import "time"
type Tag struct { type Tag struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Creator int64 `json:"creator"` Creator int64 `json:"creator"`
Title string `json:"title"` Title string `json:"title"`
Name string `json:"name"` Name string `json:"name"`
Md string `json:"md"` Md string `json:"md"`
Cover string `json:"cover"` Cover string `json:"cover"`
Icon string `json:"icon"` Icon string `json:"icon"`
Created time.Time `json:"created"` PostCount int `json:"postCount"`
Updated time.Time `json:"updated"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type Tags []*Tag type Tags []*Tag

@ -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

@ -8,7 +8,7 @@ export const Container = (props: BoxProps) => (
pt="3" pt="3"
maxW="1200px" maxW="1200px"
mx="auto" mx="auto"
px={{ base: "4", md: "8" }} px={[0,0,4,8]}
{...props} {...props}
/> />
) )

@ -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

@ -25,9 +25,10 @@ export function MarkdownEditor(props) {
return ( return (
<MdEditor <MdEditor
height="100%"
width="100%" width="100%"
value={props.md} value={props.md}
style={{ height: "102%" }} style={{ height: "102%"}}
renderHTML={_ => null} renderHTML={_ => null}
onChange={handleEditorChange} onChange={handleEditorChange}
config={{ config={{

@ -11,6 +11,7 @@ type Props = PropsOf<typeof chakra.div> & {
fontSize?: string fontSize?: string
} }
const ChakraMarkdown = chakra(Markdown)
export function MarkdownRender({ md,fontSize, ...rest }:Props) { export function MarkdownRender({ md,fontSize, ...rest }:Props) {
const rootRef = useRef<HTMLDivElement>(); const rootRef = useRef<HTMLDivElement>();
@ -23,10 +24,11 @@ export function MarkdownRender({ md,fontSize, ...rest }:Props) {
return ( return (
<div ref={rootRef} style={{height:'100%'}}> <div ref={rootRef} style={{height:'100%'}}>
<Markdown <ChakraMarkdown
children={md} children={md}
{...rest} {...rest}
style={{height:'100%',fontSize: fontSize??'14px'}} style={{height:'100%',fontSize: fontSize??'16px'}}
className="markdown-render"
options={{ options={{
overrides: { overrides: {
WebsiteLink: { WebsiteLink: {
@ -34,7 +36,7 @@ export function MarkdownRender({ md,fontSize, ...rest }:Props) {
}, },
}, },
}} }}
></Markdown> ></ChakraMarkdown>
</div> </div>
); );
} }

@ -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

@ -11,4 +11,8 @@ export interface Post {
cover?: string cover?: string
brief?: string brief?: string
created?: string created?: string
tags?: number[]
likes? : number
liked? : boolean
recommands? : number
} }

@ -6,4 +6,5 @@ export interface Tag {
icon?: string icon?: string
cover?: string cover?: string
created?: string created?: string
postCount?: number
} }

@ -4,7 +4,8 @@ export let config = {
posts: { posts: {
titleMaxLen: 128, titleMaxLen: 128,
briefMaxLen: 128, briefMaxLen: 128,
writingEnabled: false writingEnabled: false,
maxTags: 0
} }
} }

@ -1,6 +1,7 @@
import { extendTheme } from "@chakra-ui/react" import { extendTheme } from "@chakra-ui/react"
import { mode } from "@chakra-ui/theme-tools" import { mode } from "@chakra-ui/theme-tools"
import reactMarkdownStyls from 'theme/react-markdown-editor' import markdownEditor from 'theme/markdown-editor'
import markdownRender from 'theme/markdown-render'
import layerStyles from 'theme/layer-styles' import layerStyles from 'theme/layer-styles'
const customTheme = extendTheme({ const customTheme = extendTheme({
@ -22,7 +23,7 @@ const customTheme = extendTheme({
styles: { styles: {
global: (props) => ({ global: (props) => ({
body: { body: {
background: mode("gray.50","gray.800" )(props), background: mode("white","gray.800" )(props),
color: mode("gray.700", "whiteAlpha.900")(props), color: mode("gray.700", "whiteAlpha.900")(props),
".deleted": { ".deleted": {
color: "#ff8383 !important", color: "#ff8383 !important",
@ -33,7 +34,8 @@ const customTheme = extendTheme({
fontStyle: "normal !important", fontStyle: "normal !important",
}, },
}, },
...reactMarkdownStyls(props) ...markdownEditor(props),
...markdownRender(props)
}), }),
}, },
textStyles: { textStyles: {

@ -1,8 +1,7 @@
import { mode } from "@chakra-ui/theme-tools" import { mode } from "@chakra-ui/theme-tools"
import userCustomTheme from "./user-custom" import userCustomTheme from "./user-custom"
export default function reactMarkdownStyles(props) { export default function markdownEditor(props) {
console.log(props)
return { return {
'.rc-md-editor': { '.rc-md-editor': {
borderWidth: '0px', borderWidth: '0px',
@ -10,6 +9,7 @@ export default function reactMarkdownStyles(props) {
textarea: { textarea: {
background: 'transparent!important', background: 'transparent!important',
color: mode("#2D3748!important", "rgba(255, 255, 255, 0.92)!important")(props), color: mode("#2D3748!important", "rgba(255, 255, 255, 0.92)!important")(props),
fontSize: '16px !important'
}, },
'.rc-md-navigation' :{ '.rc-md-navigation' :{
background: 'transparent', background: 'transparent',

@ -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…
Cancel
Save