pull/50/head
sunface 4 years ago
parent e5511c3790
commit 0dbe149de9

@ -21,61 +21,30 @@ import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import Logo, { LogoIcon } from "src/components/logo" import Logo, { LogoIcon } from "src/components/logo"
import RadioCard from "components/radio-card"
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" import DarkMode from "components/dark-mode"
import EditModeSelect from "components/edit-mode-select" import EditModeSelect from "components/edit-mode-select"
import Tags from "components/tags/tags"
import { Post } from "src/types/posts"
interface Props {
ar : Post
changeTitle: any
changeEditMode: any
publish: any
onChange:any
}
function HeaderContent(props: any) { function HeaderContent(props: Props) {
const [tags,setTags]:[Tag[],any] = useState([])
const [allTags,setAllTags] = useState([])
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 addTag = t => { const onTagsChange = ids => {
setTags(t)
const ids = []
t.forEach(tag => ids.push(tag.id))
props.ar.tags = ids 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 (
<> <>
<Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}> <Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}>
@ -128,17 +97,7 @@ function HeaderContent(props: any) {
<Heading size="xs"> <Heading size="xs">
</Heading> </Heading>
<TagInput options={allTags} selected={tags} onChange={addTag}/> <Tags tags={props.ar.tags} onChange={onTagsChange}/>
{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>

@ -43,7 +43,7 @@ function PageContainer1(props: PageContainerProps) {
/> />
<Flex px={[0,0,16,16]}> <Flex px={[0,0,16,16]}>
<VerticalNav width={["100px","100px","200px","200px"]}/> <VerticalNav width={["100px","100px","200px","200px"]}/>
<Box width="100%" ml={["100px","100px","200px","200px"]}> <Box width="100%" ml={["100px","100px","150px","150px"]} pb="8">
{children} {children}
</Box> </Box>
</Flex> </Flex>

@ -22,6 +22,7 @@ import { Post } from "src/types/posts"
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import UnicornLike from "components/posts/unicorn-like" import UnicornLike from "components/posts/unicorn-like"
import SvgButton from "components/svg-button"
const PostPage = () => { const PostPage = () => {
const router = useRouter() const router = useRouter()
@ -35,6 +36,15 @@ const PostPage = () => {
} }
}, [id]) }, [id])
useEffect(() => {
if (router && router.asPath.indexOf("#comments") > -1) {
setTimeout(() => {
location.href = "#comments"
},100)
}
},[router])
const getData = async () => { const getData = async () => {
const res = await requestApi.get(`/post/${id}`) const res = await requestApi.get(`/post/${id}`)
setPost(res.data) setPost(res.data)
@ -58,7 +68,6 @@ const PostPage = () => {
const getComments = async (id) => { const getComments = async (id) => {
const res = await requestApi.get(`/story/comments/${id}`) const res = await requestApi.get(`/story/comments/${id}`)
console.log(res.data)
setComments(res.data) setComments(res.data)
} }
@ -96,7 +105,7 @@ const PostPage = () => {
{/* </HStack> */} {/* </HStack> */}
</Box> </Box>
<Box> <Box>
<IconButton <SvgButton
mt="6" mt="6"
aria-label="go to github" aria-label="go to github"
variant="ghost" variant="ghost"
@ -104,28 +113,30 @@ const PostPage = () => {
_focus={null} _focus={null}
fontSize="1.7rem" fontSize="1.7rem"
fontWeight="300" 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>} icon="bookmark"
onClick={null}
/> />
<Box mt="4"> <Box mt="4">
<IconButton <SvgButton
aria-label="go to github" aria-label="go to github"
variant="ghost" variant="ghost"
layerStyle="textSecondary" layerStyle="textSecondary"
_focus={null} _focus={null}
fontWeight="300" 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>} icon="share"
onClick={() => location.href="#comments"}
/> />
</Box> </Box>
{post.creatorId === session?.user.id && <Box mt="4"> {post.creatorId === session?.user.id && <Box mt="4">
<IconButton <SvgButton
aria-label="go to github" aria-label="go to github"
variant="ghost" variant="ghost"
layerStyle="textSecondary" layerStyle="textSecondary"
_focus={null} _focus={null}
fontWeight="300" fontWeight="300"
onClick={() => router.push(`${ReserveUrls.Editor}/post/${post.id}`)} onClick={() => router.push(`${ReserveUrls.Editor}/post/${post.id}`)}
icon={<svg height="1.5rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>} icon="edit"
/> />
</Box>} </Box>}
</Box> </Box>

@ -1,4 +1,4 @@
import { Box, chakra, Flex, HStack, VStack, Image, Heading, Text, Button, useColorModeValue, Divider } from "@chakra-ui/react" import { Box, chakra, Flex, HStack, VStack, Image, Heading, Text, Button, useColorModeValue, Divider, Wrap, Avatar, Center } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Container from "components/container" import Container from "components/container"
import SEO from "components/seo" import SEO from "components/seo"
@ -10,23 +10,38 @@ import PageContainer from "layouts/page-container"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { FaDove, FaEdit, FaFacebook, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa" import { FaComment, FaCommentAlt, FaDove, FaEdit, FaFacebook, FaFile, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import { User } from "src/types/session" import { User } from "src/types/session"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import moment from 'moment' import moment from 'moment'
import { Post } from "src/types/posts"
import PostCard from "components/posts/post-card"
import userCustomTheme from "theme/user-custom"
import Posts from "components/posts/posts"
import Link from "next/link"
const UserPage = () => { const UserPage = () => {
const router = useRouter() const router = useRouter()
const username = router.query.username const username = router.query.username
const session = useSession() const session = useSession()
const [user, setUser]: [User, any] = useState(null) const [user, setUser]: [User, any] = useState(null)
const [posts, setPosts]: [Post[], any] = useState([])
const borderColor = useColorModeValue('white', 'transparent') const borderColor = useColorModeValue('white', 'transparent')
useEffect(() => { useEffect(() => {
if (username) { if (username) {
requestApi.get(`/user/info/${username}`).then(res => setUser(res.data)) initData(username)
} }
}, [username]) }, [username])
const initData = async (username) => {
const res = await requestApi.get(`/user/info/${username}`)
setUser(res.data)
const res1 = await requestApi.get(`/user/posts/${res.data.id}`)
setPosts(res1.data)
}
return ( return (
<> <>
<SEO <SEO
@ -43,15 +58,15 @@ const UserPage = () => {
<Image src={user.avatar} height="130px" borderRadius="50%" border={`4px solid ${borderColor}`} /> <Image src={user.avatar} height="130px" borderRadius="50%" border={`4px solid ${borderColor}`} />
<Heading fontSize="1.8rem">{user.nickname}</Heading> <Heading fontSize="1.8rem">{user.nickname}</Heading>
{user.tagline && <Text layerStyle="textSecondary" fontWeight="450" fontSize="1.2rem" ml="1" mt="2">{user.tagline}</Text>} {user.tagline && <Text layerStyle="textSecondary" fontWeight="450" fontSize="1.2rem" ml="1" mt="2">{user.tagline}</Text>}
<Flex layerStyle="textSecondary" spacing="2" pt="3" alignItems="center"> <Flex layerStyle="textSecondary" spacing="2" pt="1" alignItems="center">
<chakra.span><FaHeart /></chakra.span><chakra.span ml="1">Followers <chakra.a fontWeight="600">0</chakra.a></chakra.span> <chakra.span><FaHeart /></chakra.span><chakra.span ml="1">Followers <chakra.a fontWeight="600">0</chakra.a></chakra.span>
<chakra.span ml="5"><FaStar /></chakra.span><chakra.span ml="1">Following <chakra.a fontWeight="600">0</chakra.a></chakra.span> <chakra.span ml="5"><FaStar /></chakra.span><chakra.span ml="1">Following <chakra.a fontWeight="600">0</chakra.a></chakra.span>
</Flex> </Flex>
<Box pt="3" position="absolute" right="15px" top="60px">{session?.user.id === user.id ? <Button onClick={() => router.push(`${ReserveUrls.Settings}/profile`)} variant="outline" leftIcon={<svg height="1.3rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>}>Edit Profile</Button> <Box pt="3" position="absolute" right="15px" top="60px">{session?.user.id === user.id ? <Button onClick={() => router.push(`${ReserveUrls.Settings}/profile`)} variant="outline" leftIcon={<svg height="1.3rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>}><chakra.span display={{base:"none",md:"block"}}>Edit Profile</chakra.span></Button>
: <Button colorScheme="teal">Follow</Button>}</Box> : <Button colorScheme="teal">Follow</Button>}</Box>
</VStack> </VStack>
</Card> </Card>
<HStack spacing="4" mt="4"> <HStack spacing={[0, 0, 4, 4]} mt="4" alignItems="top">
<VStack alignItems="left" spacing="4" width="350px" display={{ base: "none", md: "flex" }}> <VStack alignItems="left" spacing="4" width="350px" display={{ base: "none", md: "flex" }}>
<Card> <Card>
<Text layerStyle="textSecondary">{user.about}</Text> <Text layerStyle="textSecondary">{user.about}</Text>
@ -86,7 +101,44 @@ const UserPage = () => {
<Text mt="2">{user.availFor}</Text> <Text mt="2">{user.availFor}</Text>
</Box>} </Box>}
</Card> </Card>
{user.rawSkills.length > 0 && <Card>
<Heading size="md" layerStyle="textSecondary" fontWeight="500">My Tech Stack</Heading>
<Wrap mt="4" p="1">
{
user.rawSkills.map(skill =>
<Link href={`${ReserveUrls.Tags}/${skill.name}`}>
<HStack spacing="1" mr="4" mb="2" cursor="pointer">
<Avatar src={skill.icon} size="sm" />
<Text>{skill.title}</Text>
</HStack>
</Link>)
}
</Wrap>
</Card>}
{/*
<Card>
<VStack alignItems="left" spacing="3">
<HStack spacing="4"><FaFile opacity="0.7" /><Text>2 posts written</Text></HStack>
<HStack spacing="4"><FaComment opacity="0.7" /><Text>30 comments written</Text></HStack>
</VStack>
</Card> */}
</VStack> </VStack>
{
posts.length === 0 ?
<Card width="100%" height="fit-content">
<VStack spacing="16" py="16">
<Text fontSize="1.2rem">There doesn't seem to be anything here!</Text>
<Image src="/not-found.png" width="300px" />
</VStack>
</Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} />
</Card>
}
</HStack> </HStack>
</Box> </Box>
} }

@ -1,27 +1,112 @@
import { chakra } from "@chakra-ui/react" import { AddIcon } from "@chakra-ui/icons"
import {
Box, Button, chakra, Flex, HStack, VStack, Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
Heading,
Divider
} from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import PostCard from "components/posts/post-card"
import Posts from "components/posts/posts"
import SimplePostCard from "components/posts/simple-post-card"
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 PageContainer1 from "layouts/page-container1"
import PageContainer from "layouts/page-container" import React, { useEffect, useState } from "react"
import React from "react" import { PostFilter } from "src/types/posts"
import { requestApi } from "utils/axios/request"
const HomePage = () => (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer>
<Card width="200px">
<chakra.h1>NOT FOUND</chakra.h1>
<p>You just hit a route that doesn&#39;t exist... the sadness.</p>
</Card>
</PageContainer>
</>
)
const HomePage = () => {
const [posts,setPosts] = useState([])
const [filter, setFilter] = useState(PostFilter.Best)
const initData = async () => {
const res = await requestApi.get(`/home/posts/${filter}`)
setPosts(res.data)
}
useEffect(() => {
initData()
},[filter])
return (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer1>
<HStack alignItems="top" p="4">
<VStack alignItems="left" width={["100%", "100%", "70%", "70%"]}>
<Card p="2">
<Flex justifyContent="space-between" alignItems="center">
<HStack>
<Button _focus={null} onClick={() => setFilter(PostFilter.Best)} size="sm" colorScheme={filter === PostFilter.Best ? 'teal' : null} leftIcon={<svg fill="currentColor" height="1.4rem" viewBox="0 0 448 512"><path d="M448 281.6c0-53.27-51.98-163.13-124.44-230.4-20.8 19.3-39.58 39.59-56.22 59.97C240.08 73.62 206.28 35.53 168 0 69.74 91.17 0 209.96 0 281.6 0 408.85 100.29 512 224 512c.53 0 1.04-.08 1.58-.08.32 0 .6.08.92.08 1.88 0 3.71-.35 5.58-.42C352.02 507.17 448 406.04 448 281.6zm-416 0c0-50.22 47.51-147.44 136.05-237.09 27.38 27.45 52.44 56.6 73.39 85.47l24.41 33.62 26.27-32.19a573.83 573.83 0 0130.99-34.95C379.72 159.83 416 245.74 416 281.6c0 54.69-21.53 104.28-56.28 140.21 12.51-35.29 10.88-75.92-8.03-112.02a357.34 357.34 0 00-10.83-19.19l-22.63-37.4-28.82 32.87-25.86 29.5c-24.93-31.78-59.31-75.5-63.7-80.54l-24.65-28.39-24.08 28.87C108.16 287 80 324.21 80 370.41c0 19.02 3.62 36.66 9.77 52.79C54.17 387.17 32 337.03 32 281.6zm193.54 198.32C162.86 479.49 112 437.87 112 370.41c0-33.78 21.27-63.55 63.69-114.41 6.06 6.98 86.48 109.68 86.48 109.68l51.3-58.52a334.43 334.43 0 019.87 17.48c23.92 45.66 13.83 104.1-29.26 134.24-17.62 12.33-39.14 19.71-62.37 20.73-2.06.07-4.09.29-6.17.31z"></path></svg>} variant="ghost" >Best</Button>
<Button _focus={null} onClick={() => setFilter(PostFilter.Featured)} size="sm" colorScheme={filter === PostFilter.Featured ? 'teal' : null} leftIcon={<svg fill="currentColor" height="1.4rem" viewBox="0 0 512 512"><path d="M493.7 232.4l-140.2-35 66.9-83.3c5.2-6.5 4.7-15.5-1.1-21.3-5.9-5.8-14.8-6.3-21.3-1.1l-83.4 66.7-35-140c-6.1-24.4-41-24.4-47.2 0l-35 140.2-83.3-67c-6.5-5.2-15.5-4.8-21.3 1.1-5.8 5.8-6.3 14.8-1.1 21.4l66.7 83.4-140 35C7.5 235.2 0 244.7 0 256c0 10.2 6.5 20.7 18.4 23.6l140.2 35-66.9 83.3c-5.2 6.5-4.7 15.5 1.1 21.3 5.6 5.5 14.5 6.5 21.3 1.1l83.4-66.7 35 140c3 11.9 13.3 18.4 23.6 18.4 4.5 0 19.4-2.1 23.6-18.4l35-140.2 83.3 67c6.9 5.5 15.8 4.4 21.3-1.1 5.8-5.8 6.3-14.8 1.1-21.3l-66.7-83.4 139.9-35c11.7-2.9 18.5-13.1 18.5-23.6-.1-10.3-6.6-20.6-18.4-23.6zM296 296l-40 160-40-160-160-40 160-40 40-160 40 160 160 40-160 40z"></path></svg>} variant="ghost">Fetured</Button>
<Button _focus={null} onClick={() => setFilter(PostFilter.Recent)} size="sm" colorScheme={filter === PostFilter.Recent ? 'teal' : null} leftIcon={<svg fill="currentColor" height="1.4rem" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm216 248c0 118.7-96.1 216-216 216-118.7 0-216-96.1-216-216 0-118.7 96.1-216 216-216 118.7 0 216 96.1 216 216zm-148.9 88.3l-81.2-59c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h14c6.6 0 12 5.4 12 12v146.3l70.5 51.3c5.4 3.9 6.5 11.4 2.6 16.8l-8.2 11.3c-3.9 5.3-11.4 6.5-16.8 2.6z"></path></svg>} variant="ghost">Fetured</Button>
</HStack>
<Menu>
<MenuButton
as={IconButton}
aria-label="Options"
icon={<svg fill="none" stroke="currentColor" opacity="0.75" height="1.3rem" viewBox="0 0 55 55"><path d="M2 2h51v21H2V2zm0 30h51v21H2V32z" stroke="stroke-current" strokeWidth="4"></path></svg>}
size="xs"
variant="ghost"
_focus={null}
/>
<MenuList>
<MenuItem icon={<svg fill="none" stroke="currentColor" opacity="0.75" height="1.3rem" viewBox="0 0 55 55"><path d="M2 2h51v21H2V2zm0 30h51v21H2V32z" stroke="stroke-current" strokeWidth="4"></path></svg>}>
Modern
</MenuItem>
<MenuItem icon={<svg stroke="currentColor" height="1.2rem" viewBox="0 0 55 55" fill="none"><path d="M2 2h51v11H2V2zm0 40h51v11H2V42zm0-20h51v11H2V22z" stroke="stoke-current" strokeWidth="4"></path></svg>}>
Compact
</MenuItem>
</MenuList>
</Menu>
</Flex>
</Card>
<Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} />
</Card>
</VStack>
<HomeSidebar />
</HStack>
</PageContainer1>
</>
)
}
export default HomePage export default HomePage
export const HomeSidebar = () => {
const [posts,setPosts] = useState([])
const [filter, setFilter] = useState(PostFilter.Best)
const initData = async () => {
const res = await requestApi.get(`/home/posts/${filter}`)
setPosts(res.data)
}
useEffect(() => {
initData()
},[filter])
return (
<VStack alignItems="left" width="30%" display={{ base: "none", md: "flex" }}>
<Card p="0">
<HStack px="4" py="3">
<Heading size="sm">Top ariticles</Heading>
<Button variant="ghost" size="sm">1d</Button>
<Button variant="ghost" size="sm">1w</Button>
<Button variant="ghost" size="sm">1m</Button>
</HStack>
<Divider />
<VStack px="4" pt="1" alignItems="left">
<Posts posts={posts} card={SimplePostCard} size="sm" showFooter={false}></Posts>
</VStack>
</Card>
</VStack>
)
}

@ -16,10 +16,12 @@ import { route } from "next/dist/next-server/server/router"
import { Field, Form, Formik } from "formik" import { Field, Form, Formik } from "formik"
import useSession from "hooks/use-session" import useSession from "hooks/use-session"
import { config } from "utils/config" import { config } from "utils/config"
import Tags from "components/tags/tags"
var validator = require('validator'); var validator = require('validator');
const UserProfilePage = () => { const UserProfilePage = () => {
const [user, setUser] = useState(null) const [user, setUser] = useState(null)
const [skills, setSkills] = useState([])
const [isLargerThan1280] = useMediaQuery("(min-width: 768px)") const [isLargerThan1280] = useMediaQuery("(min-width: 768px)")
useEffect(() => { useEffect(() => {
requestApi.get("/user/self").then(res => setUser(res.data)) requestApi.get("/user/self").then(res => setUser(res.data))
@ -28,7 +30,7 @@ const UserProfilePage = () => {
const toast = useToast() const toast = useToast()
const submitUser = async (values, _) => { const submitUser = async (values, _) => {
await requestApi.post(`/user/update`,values) await requestApi.post(`/user/update`, values)
setUser(values) setUser(values)
toast({ toast({
description: "更新成功", description: "更新成功",
@ -70,7 +72,7 @@ const UserProfilePage = () => {
} }
function validateUrl(value, canBeEmpty=true) { function validateUrl(value, canBeEmpty = true) {
let url = value?.trim() let url = value?.trim()
let error let error
if (!canBeEmpty) { if (!canBeEmpty) {
@ -115,7 +117,7 @@ const UserProfilePage = () => {
<Card p={[2, 2, 6, 6]}> <Card p={[2, 2, 6, 6]}>
<Layout spacing={isLargerThan1280 ? "8" : "6"} alignItems={isLargerThan1280 ? 'top' : 'left'}> <Layout spacing={isLargerThan1280 ? "8" : "6"} alignItems={isLargerThan1280 ? 'top' : 'left'}>
<Box width="100%"> <Box width="100%">
<VStack alignItems="left" spacing="6"> <VStack alignItems="left" spacing="6">
<Heading fontSize="1.2rem"></Heading> <Heading fontSize="1.2rem"></Heading>
<Field name="nickname" validate={validateNickname}> <Field name="nickname" validate={validateNickname}>
{({ field, form }) => ( {({ field, form }) => (
@ -199,6 +201,26 @@ const UserProfilePage = () => {
</FormControl> </FormControl>
)} )}
</Field> </Field>
<Field name="about" validate={validateLen}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.about && form.touched.about} >
<FormLabel></FormLabel>
<Textarea {...field} placeholder="give us more info about you" size="lg" />
<FormErrorMessage>{form.errors.about}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="skills" validate={validateLen}>
{({ field, form }) => (
<FormControl >
<FormLabel></FormLabel>
<Tags tags={user.skills} onChange={(v) => form.values.skills = v} size="lg"/>
</FormControl>
)}
</Field>
<Box>
</Box>
</VStack> </VStack>
</Box> </Box>
<Box width="100%" > <Box width="100%" >

@ -1,15 +1,18 @@
import { Box, Button, chakra, Flex, Heading, HStack, Image, Text } from "@chakra-ui/react" import { Box, Button, chakra, Flex, Heading, HStack, Image, Text, VStack } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Container from "components/container" import Container from "components/container"
import { MarkdownRender } from "components/markdown-editor/render" import { MarkdownRender } from "components/markdown-editor/render"
import Posts from "components/posts/posts"
import SEO from "components/seo" import SEO from "components/seo"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import useSession from "hooks/use-session" import useSession from "hooks/use-session"
import Nav from "layouts/nav/nav" import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container" import PageContainer from "layouts/page-container"
import PageContainer1 from "layouts/page-container1"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import { Post } from "src/types/posts"
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import { isAdmin } from "utils/role" import { isAdmin } from "utils/role"
@ -17,14 +20,19 @@ import { isAdmin } from "utils/role"
const UserPage = () => { const UserPage = () => {
const router = useRouter() const router = useRouter()
const [posts, setPosts]: [Post[], any] = useState([])
const [tag, setTag]: [Tag, any] = useState({}) const [tag, setTag]: [Tag, any] = useState({})
const getTag = async () => { const initData = async () => {
const res = await requestApi.get(`/tag/${router.query.name}`) const res = await requestApi.get(`/tag/info/${router.query.name}`)
setTag(res.data) setTag(res.data)
const res1 = await requestApi.get(`/tag/posts/${res.data.id}`)
setPosts(res1.data)
} }
useEffect(() => { useEffect(() => {
if (router.query.name) { if (router.query.name) {
getTag() initData()
} }
}, [router.query.name]) }, [router.query.name])
@ -36,47 +44,61 @@ const UserPage = () => {
title={siteConfig.seo.title} title={siteConfig.seo.title}
description={siteConfig.seo.description} description={siteConfig.seo.description}
/> />
<PageContainer> <PageContainer1>
{tag.name && <HStack alignItems="top" spacing="4"> {tag.name &&
<Box width="70%"> <HStack alignItems="top" spacing="4" p="2">
<Card p="0"> <VStack width={["100%","100%","70%","70%"]} alignItems="left" spacing="2">
<Image src={tag.cover} /> <Card p="0">
<Image src={tag.icon} width="80px" position="relative" top="-40px" left="40px"/> <Image src={tag.cover} />
<Flex justifyContent="space-between" alignItems="center" px="8" pb="6" mt="-1rem"> <Image src={tag.icon} width="80px" position="relative" top="-40px" left="40px" />
<Box> <Flex justifyContent="space-between" alignItems="center" px="8" pb="6" mt="-1rem">
<Heading size="lg">{tag.title}</Heading> <Box>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">#{tag.name}</Text> <Heading size="lg">{tag.title}</Heading>
</Box> <Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">#{tag.name}</Text>
<Box> </Box>
<Button colorScheme="teal">Follow</Button> <Box>
{isAdmin(session.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>} <Button colorScheme="teal">Follow</Button>
</Box> {isAdmin(session?.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>}
</Flex> </Box>
</Flex>
</Card> </Card>
</Box> {
<Box width="30%"> posts.length === 0 ?
<Card> <Card width="100%" height="fit-content">
<Flex justifyContent="space-between" alignItems="center" px={[0,2,4,8]}> <VStack spacing="16" py="16">
<Box> <Text fontSize="1.2rem">There doesn't seem to be anything here!</Text>
<Heading size="lg">59.8K</Heading> <Image src="/not-found.png" width="300px" />
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Followers</Text> </VStack>
</Box> </Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} />
</Card>
}
</VStack>
<VStack width="30%" alignItems="left" spacing="2" display={{base: "none",md:"flex"}}>
<Card>
<Flex justifyContent="space-between" alignItems="center" px={[0, 2, 4, 8]}>
<Box>
<Heading size="lg">59.8K</Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Followers</Text>
</Box>
<Box> <Box>
<Heading size="lg">{tag.postCount}</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>
</Card> </Card>
<Card mt="4"> <Card mt="4">
<Heading size="sm">About this tag</Heading> <Heading size="sm">About this tag</Heading>
<Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box> <Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box>
</Card> </Card>
</Box> </VStack>
</HStack>} </HStack>}
</PageContainer> </PageContainer1>
</> </>
) )
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

@ -2,6 +2,7 @@ package api
import ( import (
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/story" "github.com/imdotdev/im.dev/server/internal/story"
@ -83,3 +84,60 @@ func LikeStory(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func GetUserPosts(c *gin.Context) {
userID, _ := strconv.ParseInt(c.Param("userID"), 10, 64)
posts, err := story.UserPosts(userID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user != nil {
for _, post := range posts {
post.Liked = story.GetLiked(post.ID, user.ID)
}
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}
func GetTagPosts(c *gin.Context) {
tagID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
posts, err := story.TagPosts(tagID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user != nil {
for _, post := range posts {
post.Liked = story.GetLiked(post.ID, user.ID)
}
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}
func GetHomePosts(c *gin.Context) {
filter := c.Param("filter")
posts, err := story.HomePosts(filter)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user != nil {
for _, post := range posts {
post.Liked = story.GetLiked(post.ID, user.ID)
}
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}

@ -55,6 +55,7 @@ func (s *Server) Start() error {
r.POST("/story/like/:id", IsLogin(), api.LikeStory) r.POST("/story/like/:id", IsLogin(), api.LikeStory)
r.GET("/story/comments/:id", api.GetStoryComments) r.GET("/story/comments/:id", api.GetStoryComments)
r.POST("/story/comment", IsLogin(), api.SubmitComment) r.POST("/story/comment", IsLogin(), api.SubmitComment)
r.DELETE("/comment/:id", IsLogin(), api.DeleteComment) r.DELETE("/comment/:id", IsLogin(), api.DeleteComment)
r.GET("/editor/posts", IsLogin(), api.GetEditorPosts) r.GET("/editor/posts", IsLogin(), api.GetEditorPosts)
@ -66,12 +67,17 @@ func (s *Server) Start() error {
r.DELETE("/admin/tag/:id", IsLogin(), api.DeleteTag) r.DELETE("/admin/tag/:id", IsLogin(), api.DeleteTag)
r.GET("/tags", api.GetTags) r.GET("/tags", api.GetTags)
r.GET("/tag/:name", api.GetTag) r.GET("/tag/posts/:id", api.GetTagPosts)
r.GET("/tag/info/:name", api.GetTag)
r.GET("/users", api.GetUsers) r.GET("/users", api.GetUsers)
r.GET("/user/self", IsLogin(), api.GetUserSelf) r.GET("/user/self", IsLogin(), api.GetUserSelf)
r.GET("/user/info/:username", api.GetUser) r.GET("/user/info/:username", api.GetUser)
r.POST("/user/update", IsLogin(), api.UpdateUser) r.POST("/user/update", IsLogin(), api.UpdateUser)
r.GET("/user/posts/:userID", api.GetUserPosts)
r.GET("/home/posts/:filter", api.GetHomePosts)
r.GET("/session", IsLogin(), api.GetSession) r.GET("/session", IsLogin(), api.GetSession)
err := router.Run(config.Data.Server.Addr) err := router.Run(config.Data.Server.Addr)
if err != nil { if err != nil {

@ -38,10 +38,20 @@ var sqlTables = map[string]string{
weibo VARCHAR(255), weibo VARCHAR(255),
facebook VARCHAR(255), facebook VARCHAR(255),
stackoverflow VARCHAR(255), stackoverflow VARCHAR(255),
updated DATETIME updated DATETIME
);`, );`,
"user_skills": `CREATE TABLE IF NOT EXISTS user_skills (
user_id INTEGER,
skill_id INTEGER
);
CREATE INDEX IF NOT EXISTS user_skills_userid
ON user_skills (user_id);
CREATE INDEX IF NOT EXISTS user_skills_skillid
ON user_skills (skill_id);
`,
"sessions": `CREATE TABLE IF NOT EXISTS sessions ( "sessions": `CREATE TABLE IF NOT EXISTS sessions (
sid VARCHAR(255) primary key, sid VARCHAR(255) primary key,
user_id INTEGER user_id INTEGER
@ -49,19 +59,20 @@ var sqlTables = map[string]string{
`, `,
"posts": `CREATE TABLE IF NOT EXISTS posts ( "posts": `CREATE TABLE IF NOT EXISTS posts (
id VARCHAR(255) PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
creator INTEGER NOT NULL, creator INTEGER NOT NULL,
slug VARCHAR(64) NOT NULL, slug VARCHAR(64) NOT NULL,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
md TEXT, md TEXT,
url VARCHAR(255), url VARCHAR(255),
cover VARCHAR(255), cover VARCHAR(255),
brief TEXT, brief TEXT,
likes INTEGER DEFAULT 0, likes INTEGER DEFAULT 0,
views INTEGER DEFAULT 0, views INTEGER DEFAULT 0,
status tinyint NOT NULL, comments INTEGER DEFAULT 0,
created DATETIME NOT NULL, status tinyint NOT NULL,
updated DATETIME created DATETIME NOT NULL,
updated DATETIME
); );
CREATE INDEX IF NOT EXISTS posts_creator CREATE INDEX IF NOT EXISTS posts_creator
ON posts (creator); ON posts (creator);
@ -124,4 +135,10 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS comments_creator CREATE INDEX IF NOT EXISTS comments_creator
ON comments (creator); ON comments (creator);
`, `,
"comments_count": `CREATE TABLE IF NOT EXISTS comments_count (
story_id VARCHAR(255) PRIMARY KEY,
count INTEGER DEFAULT 0
);
`,
} }

@ -2,6 +2,7 @@ package story
import ( import (
"database/sql" "database/sql"
"errors"
"net/http" "net/http"
"sort" "sort"
"time" "time"
@ -23,6 +24,36 @@ func AddComment(c *models.Comment) *e.Error {
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
} }
// 更新story的comment数量
// 查询到该comment所属的story id
var storyID string
err = db.Conn.QueryRow("select target_id from comments where id=?", c.TargetID).Scan(&storyID)
if err != nil && err != sql.ErrNoRows {
logger.Warn("select comment error", "error", err)
} else {
if storyID == "" {
storyID = c.TargetID
}
var nid string
err := db.Conn.QueryRow("SELECT story_id FROM comments_count WHERE story_id=?", storyID).Scan(&nid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("select from comments_count error", "error", err)
return nil
}
if err == sql.ErrNoRows {
_, err := db.Conn.Exec("INSERT INTO comments_count (story_id,count) VALUES(?,?)", storyID, 1)
if err != nil {
logger.Warn("insert into comments_count error", "error", err)
}
} else {
_, err := db.Conn.Exec("UPDATE comments_count SET count=count+1 WHERE story_id=?", storyID)
if err != nil {
logger.Warn("update comments_count error", "error", err)
}
}
}
return nil return nil
} }
@ -89,8 +120,34 @@ func GetComment(id string) (*models.Comment, *e.Error) {
} }
func DeleteComment(id string) *e.Error { func DeleteComment(id string) *e.Error {
// 更新story的comment数量
// 查询到该comment所属的story id
storyID, isComment, err := GetStoryIDByCommentID(id)
if err != nil {
logger.Warn("delete comment error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
count := 0
if isComment {
// 如果是评论我们要计算replies的数量因为会一起删除
err := db.Conn.QueryRow("select count(*) from comments where target_id=?", id).Scan(&count)
if err != nil && err != sql.ErrNoRows {
logger.Warn("select comment error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
}
count += 1
_, err = db.Conn.Exec("UPDATE comments_count SET count=count-? WHERE story_id=?", count, storyID)
if err != nil {
logger.Warn("update comments_count error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
// delete children replies // delete children replies
_, err := db.Conn.Exec("DELETE FROM comments WHERE target_id=?", id) _, err = db.Conn.Exec("DELETE FROM comments WHERE target_id=?", id)
if err != nil { if err != nil {
logger.Warn("delete comment replies error", "error", err) logger.Warn("delete comment replies error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
@ -119,3 +176,36 @@ func commentExist(id string) bool {
return false return false
} }
func GetCommentCount(storyID string) int {
count := 0
err := db.Conn.QueryRow("SELECT count from comments_count WHERE story_id=?", storyID).Scan(&count)
if err != nil && err != sql.ErrNoRows {
logger.Warn("query comment count error", "error", err)
}
return count
}
func GetStoryIDByCommentID(cid string) (string, bool, error) {
var targetID string
err := db.Conn.QueryRow("select target_id from comments where id=?", cid).Scan(&targetID)
if err != nil {
return "", false, err
}
switch targetID[:1] {
case models.StoryPost:
return targetID, true, nil
case models.StoryComment:
var nid string
err := db.Conn.QueryRow("select target_id from comments where id=?", targetID).Scan(&nid)
if err != nil {
return "", false, err
}
return nid, false, nil
default:
return "", false, errors.New("bad comment id")
}
}

@ -23,10 +23,7 @@ import (
func UserPosts(uid int64) (models.Posts, *e.Error) { func UserPosts(uid int64) (models.Posts, *e.Error) {
ars := make(models.Posts, 0) ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,created,updated from posts where creator=?", uid) rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,created,updated from posts where creator=?", uid)
if err != nil { if err != nil && err != sql.ErrNoRows {
if err == sql.ErrNoRows {
return ars, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get user posts error", "error", err) logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal) return ars, e.New(http.StatusInternalServerError, e.Internal)
} }
@ -42,6 +39,55 @@ func UserPosts(uid int64) (models.Posts, *e.Error) {
} }
ar.Creator = creator ar.Creator = creator
ar.Comments = GetCommentCount(ar.ID)
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func TagPosts(tagID int64) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
// get post ids
rows, err := db.Conn.Query("select post_id from tag_post where tag_id=?", tagID)
if err != nil {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
postIDs := make([]string, 0)
for rows.Next() {
var id string
rows.Scan(&id)
postIDs = append(postIDs, id)
}
ids := strings.Join(postIDs, "','")
q := fmt.Sprintf("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts where id in ('%s')", ids)
rows, err = db.Conn.Query(q)
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Likes, &ar.Views, &ar.CreatorID, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
creator := &models.UserSimple{ID: ar.CreatorID}
creator.Query()
ar.Creator = creator
ar.Comments = GetCommentCount(ar.ID)
ars = append(ars, ar) ars = append(ars, ar)
} }
@ -124,10 +170,6 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
} }
//update tags //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) _, err = db.Conn.Exec("DELETE FROM tag_post WHERE post_id=?", post.ID)
if err != nil { if err != nil {
logger.Warn("delete post tags error", "error", err) logger.Warn("delete post tags error", "error", err)
@ -153,6 +195,12 @@ func DeletePost(id string) *e.Error {
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
} }
// delete tags
_, err = db.Conn.Exec("DELETE FROM tag_post WHERE post_id=?", id)
if err != nil {
logger.Warn("delete post tags error", "error", err)
}
return nil return nil
} }
@ -218,6 +266,35 @@ func GetPostCreator(id string) (int64, *e.Error) {
return uid, nil return uid, nil
} }
func HomePosts(filter string) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts")
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Likes, &ar.Views, &ar.CreatorID, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
creator := &models.UserSimple{ID: ar.CreatorID}
creator.Query()
ar.Creator = creator
ar.Comments = GetCommentCount(ar.ID)
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func postExist(id string) bool { func postExist(id string) bool {
var nid string var nid string
err := db.Conn.QueryRow("SELECT id from posts WHERE id=?", id).Scan(&nid) err := db.Conn.QueryRow("SELECT id from posts WHERE id=?", id).Scan(&nid)

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/imdotdev/im.dev/server/internal/cache" "github.com/imdotdev/im.dev/server/internal/cache"
"github.com/imdotdev/im.dev/server/internal/tags"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models" "github.com/imdotdev/im.dev/server/pkg/models"
@ -52,6 +53,26 @@ func GetUserDetail(id int64, username string) (*models.User, *e.Error) {
user.Cover = models.DefaultCover user.Cover = models.DefaultCover
} }
// get user skills
user.Skills = make([]int64, 0)
user.RawSkills = make([]*models.Tag, 0)
rows, err := db.Conn.Query("SELECT skill_id from user_skills WHERE user_id=?", user.ID)
if err != nil && err != sql.ErrNoRows {
logger.Warn("query user skills error", "error", err)
}
for rows.Next() {
var skill int64
rows.Scan(&skill)
user.Skills = append(user.Skills, skill)
rawTag, err := tags.GetTag(skill, "")
if err != nil {
logger.Warn("get tag error", "error", err)
continue
}
user.RawSkills = append(user.RawSkills, rawTag)
}
return user, nil return user, nil
} }
@ -86,5 +107,18 @@ func UpdateUser(u *models.User) *e.Error {
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
} }
//update user skills
_, err = db.Conn.Exec("DELETE FROM user_skills WHERE user_id=?", u.ID)
if err != nil {
logger.Warn("delete user skills error", "error", err)
}
for _, skill := range u.Skills {
_, err = db.Conn.Exec("INSERT INTO user_skills (user_id,skill_id) VALUES (?,?)", u.ID, skill)
if err != nil {
logger.Warn("add user skill error", "error", err)
}
}
return nil return nil
} }

@ -22,6 +22,7 @@ type Post struct {
RawTags []*Tag `json:"rawTags"` RawTags []*Tag `json:"rawTags"`
Likes int `json:"likes"` Likes int `json:"likes"`
Liked bool `json:"liked"` Liked bool `json:"liked"`
Comments int `json:"comments"`
Views int `json:"views"` Views int `json:"views"`
Status int `json:"status"` Status int `json:"status"`
Created time.Time `json:"created"` Created time.Time `json:"created"`

@ -14,12 +14,13 @@ type User struct {
Email string `json:"email"` Email string `json:"email"`
Role RoleType `json:"role"` Role RoleType `json:"role"`
Tagline string `json:"tagline"` Tagline string `json:"tagline"`
Cover string `json:"cover"` Cover string `json:"cover"`
Location string `json:"location"` Location string `json:"location"`
AvailFor string `json:"availFor"` AvailFor string `json:"availFor"`
About string `json:"about"` About string `json:"about"`
Skills string `json:"skills"` RawSkills []*Tag `json:"rawSkills"`
Skills []int64 `json:"skills"`
Website string `json:"website"` Website string `json:"website"`
Twitter string `json:"twitter"` Twitter string `json:"twitter"`
@ -34,7 +35,8 @@ type User struct {
} }
const DefaultAvatar = "https://cdn.hashnode.com/res/hashnode/image/upload/v1600792675173/rY-APy9Fc.png?auto=compress" const DefaultAvatar = "https://cdn.hashnode.com/res/hashnode/image/upload/v1600792675173/rY-APy9Fc.png?auto=compress"
const DefaultCover = "https://cdn.hashnode.com/res/hashnode/image/upload/v1584035951809/rA6njTVVd.jpeg?w=1600&fit=crop&crop=entropy&auto=compress&auto=compress" const DefaultCover = "https://cdn.hashnode.com/res/hashnode/image/upload/v1604243390177/JstCbDgbK.jpeg?w=1600&fit=crop&crop=entropy&auto=compress"
func (user *User) Query(id int64, username string, email string) error { func (user *User) Query(id int64, username string, email string) error {
err := db.Conn.QueryRow(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user WHERE id=? or username=? or email=?`, err := db.Conn.QueryRow(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user WHERE id=? or username=? or email=?`,
id, username, email).Scan(&user.ID, &user.Username, &user.Role, &user.Nickname, &user.Email, &user.Avatar, &user.LastSeenAt, &user.Created) id, username, email).Scan(&user.ID, &user.Username, &user.Role, &user.Nickname, &user.Email, &user.Avatar, &user.LastSeenAt, &user.Created)

@ -31,7 +31,7 @@ export const Comments = ({storyID, comments,onChange }: Props) => {
} }
return ( return (
<VStack spacing="4" alignItems="left"> <VStack spacing="4" alignItems="left" id="comments">
<Card> <Card>
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
<HStack> <HStack>

@ -143,7 +143,7 @@ export function MarkdownRender({ md,fontSize, ...rest }:Props) {
<ChakraMarkdown <ChakraMarkdown
children={renderMd} children={renderMd}
{...rest} {...rest}
style={{height:'100%',fontSize: fontSize??'16px'}} style={{height:'100%',fontSize: fontSize??'1.1rem'}}
className="markdown-render" className="markdown-render"
options={{ options={{
overrides: { overrides: {

@ -1,5 +1,5 @@
import React from "react" import React from "react"
import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack} from "@chakra-ui/react" import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack, propNames} from "@chakra-ui/react"
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import NextLink from "next/link" import NextLink from "next/link"
@ -10,25 +10,24 @@ import Link from "next/link"
import { useRouter } from "next/router" import { useRouter } from "next/router"
type Props = PropsOf<typeof chakra.div> & { type Props = PropsOf<typeof chakra.div> & {
size?: 'lg' | 'md'
post : Post post : Post
showFooter?: boolean
} }
export const PostAuthor= ({post}:Props) =>{ export const PostAuthor= ({post,showFooter=true,size='lg'}:Props) =>{
const router = useRouter() const router = useRouter()
console.log(post)
return ( return (
<Flex justifyContent="space-between">
<HStack spacing="4"> <HStack spacing="4">
<Avatar src={post.creator.avatar} size="lg" onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer"/> <Avatar src={post.creator.avatar} size={size} onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1"> <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> <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> <Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600" ml="1">{moment(post.created).fromNow()}</chakra.span></Text>
<HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3"> {showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3">
<FaGithub /> <chakra.span>4 min read</chakra.span> <FaGithub /> <chakra.span>4 min read</chakra.span>
</HStack> </HStack>}
</VStack> </VStack>
</HStack> </HStack>
</Flex>
) )
} }

@ -0,0 +1,56 @@
import React from "react"
import { Box, chakra, Flex, Heading, HStack, Image, Text, useMediaQuery, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts"
import PostAuthor from "./post-author"
import Link from "next/link"
import UnicornLike from "./heart-like"
import { FaHeart, FaRegBookmark, FaRegComment, FaRegHeart } from "react-icons/fa"
import SvgButton from "components/svg-button"
interface Props {
post: Post
}
export const PostCard = (props: Props) => {
const { post } = props
const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack
return (
<VStack alignItems="left" spacing="4" p="1">
<PostAuthor post={post} showFooter={false} size="md" />
<Link href={`/${post.creator.username}/${post.id}`}>
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
<VStack alignItems="left" spacing="3" width={isLargeScreen ? "calc(100% - 18rem)" : '100%'}>
<Heading size="md">{post.title}</Heading>
<Text layerStyle="textSecondary">{post.brief}</Text>
</VStack>
{post.cover && <Image src={post.cover} width="18rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
</Layout>
</Link>
<HStack pl="2" spacing="5">
<HStack opacity="0.9">
{post.liked ?
<Box color="red.400"><FaHeart fontSize="1.1rem" /></Box>
:
<FaRegHeart fontSize="1.1rem" />}
<Text ml="2">{post.likes}</Text>
</HStack>
<Link href={`/${post.creator.username}/${post.id}#comments`}>
<HStack opacity="0.9" cursor="pointer">
<FaRegComment fontSize="1.1rem" />
<Text ml="2">{post.comments}</Text>
</HStack>
</Link>
<SvgButton icon="bookmark" height="1rem" onClick={null} style={{marginLeft: '4px'}}/>
</HStack>
</VStack>
)
}
export default PostCard

@ -0,0 +1,32 @@
import React from "react"
import { Box, Center, Text, useColorModeValue, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts"
import PostCard from "./post-card"
import userCustomTheme from "theme/user-custom"
interface Props {
posts: Post[]
card?: any
size?: 'sm' | 'md'
showFooter?: boolean
}
export const Posts = (props: Props) => {
const { posts,card=PostCard,showFooter=true} = props
const postBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
const Card = card
return (
<>
<VStack alignItems="left">
{posts.map(post =>
<Box py="4" borderBottom={`1px solid ${postBorderColor}`} key={post.id}>
<Card post={post} size={props.size}/>
</Box>)}
</VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>}
</>
)
}
export default Posts

@ -0,0 +1,49 @@
import React from "react"
import { Box, chakra, Flex, Heading, HStack, Image, Text, useMediaQuery, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts"
import PostAuthor from "./post-author"
import Link from "next/link"
import UnicornLike from "./heart-like"
import { FaHeart, FaRegBookmark, FaRegComment, FaRegHeart } from "react-icons/fa"
import SvgButton from "components/svg-button"
interface Props {
post: Post
size?: 'md' | 'sm'
}
export const SimplePostCard = (props: Props) => {
const { post,size='md' } = props
const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack
return (
<VStack alignItems="left" spacing="0">
<Link href={`/${post.creator.username}/${post.id}`}><Heading pb="2" size="sm" cursor="pointer">{post.title}</Heading></Link>
<HStack pl="1" spacing="5" fontSize={size==='md'? '1rem' : ".9rem"}>
<Link href={`/${post.creator.username}`}><Text cursor="pointer">{post.creator.nickname}</Text></Link>
<HStack opacity="0.9">
{post.liked ?
<Box color="red.400"><FaHeart fontSize="1.1rem" /></Box>
:
<FaRegHeart fontSize="1.1rem" />}
<Text ml="2">{post.likes}</Text>
</HStack>
<Link href={`/${post.creator.username}/${post.id}#comments`}>
<HStack opacity="0.9" cursor="pointer">
<FaRegComment fontSize="1.1rem" />
<Text ml="2">{post.comments}</Text>
</HStack>
</Link>
<SvgButton icon="bookmark" height="1rem" onClick={null} style={{marginLeft: '4px'}}/>
</HStack>
</VStack>
)
}
export default SimplePostCard

@ -0,0 +1,39 @@
import React from "react"
import {chakra, PropsOf, IconButton } from "@chakra-ui/react"
type Props = PropsOf<typeof chakra.div> & {
icon: 'bookmark' | 'edit' | 'share'
onClick: any
height?: string
}
export const SvgButton= (props:Props) =>{
const {icon,height="1.7rem",...rest} = props
let iconSvg
switch (icon) {
case "bookmark":
iconSvg = <svg height={height} 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>
break;
case "share":
iconSvg = <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>
break
case "edit":
iconSvg = <svg height="1.5rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>
break
default:
break;
}
return (
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={iconSvg}
{...rest}
/>
)
}
export default SvgButton

@ -9,6 +9,7 @@ interface Props {
options: Tag[] options: Tag[]
selected: Tag[] selected: Tag[]
onChange: any onChange: any
size?: 'lg' | 'md'
} }
@ -44,7 +45,7 @@ export const TagInput = (props: Props) => {
return ( 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" />} {props.selected.length <=config.posts.maxTags && <Input size={props.size} 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}> {tags.length > 0 && <Popover isOpen={isOpen} closeOnBlur={false} placement="bottom-start" onOpen={onOpen} onClose={onClose} autoFocus={false}>
<PopoverTrigger><Box width="100%"></Box></PopoverTrigger> <PopoverTrigger><Box width="100%"></Box></PopoverTrigger>
<PopoverContent width="100%"> <PopoverContent width="100%">

@ -0,0 +1,74 @@
import React, { useEffect, useState } from "react"
import { Box, Tag as ChakraTag, TagCloseButton, TagLabel } from "@chakra-ui/react"
import TagInput from "./tag-input"
import { requestApi } from "utils/axios/request"
import { Tag } from "src/types/tag"
import { cloneDeep, remove } from "lodash"
interface Props {
tags: number[]
onChange: any
size?: 'lg' | 'md'
}
export const Tags = (props: Props) => {
// 所有的tags选项
const [options, setOptions]: [Tag[], any] = useState([])
// 当前已选择的tags
const [tags, setTags]: [Tag[], any] = useState([])
useEffect(() => {
requestApi.get('/tags').then(res => {
setOptions(res.data)
const t = []
props.tags?.forEach(id => {
res.data.forEach(tag => {
if (tag.id === id) {
t.push(tag)
}
})
})
setTags(t)
})
}, [])
const addTag = t => {
setTags(t)
const ids = []
t.forEach(tag => ids.push(tag.id))
props.onChange(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.onChange(ids)
}
return (
<>
<TagInput options={options} selected={tags} onChange={addTag} size={props.size}/>
{tags.length > 0 && <Box mt={props.size === 'lg' ? 4 : 2}>
{
tags.map(tag =>
<ChakraTag key={tag.id} mr="2" colorScheme="teal" variant="solid" px="2" py={props.size === 'lg' ? 2 : 1}>
<TagLabel>{tag.title}</TagLabel>
<TagCloseButton onClick={_ => removeTag(tag)} />
</ChakraTag>)
}
</Box>
}
</>
)
}
export default Tags

@ -1,6 +1,12 @@
import { UserSimple} from './session' import { UserSimple} from './session'
import { Tag } from './tag'; import { Tag } from './tag';
export enum PostFilter {
Best = "best",
Featured = "featured",
Recent = "recent"
}
export interface Post { export interface Post {
id?: string id?: string
slug?: string slug?: string
@ -14,7 +20,7 @@ export interface Post {
created?: string created?: string
tags?: number[] tags?: number[]
rawTags?: Tag[] rawTags?: Tag[]
likes? : number likes? : number
liked? : boolean liked? : boolean
recommands? : number comments? : number
} }

@ -20,7 +20,8 @@ export interface User {
location?: string location?: string
availFor?: string availFor?: string
about?: string about?: string
skills?: Tag[] rawSkills?: Tag[]
skills?: number[]
// social links // social links
website?: string website?: string

@ -33,7 +33,8 @@ const customTheme = extendTheme({
color: "#b5f4a5 !important", color: "#b5f4a5 !important",
fontStyle: "normal !important", fontStyle: "normal !important",
}, },
fontWeight: '450' fontWeight: '450',
fontSize: '17px'
}, },
...markdownEditor(props), ...markdownEditor(props),
...markdownRender(props) ...markdownRender(props)

Loading…
Cancel
Save