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 React, { useEffect, useState } from "react"
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 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 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) {
const [tags,setTags]:[Tag[],any] = useState([])
const [allTags,setAllTags] = useState([])
function HeaderContent(props: Props) {
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 => {
setTags(t)
const ids = []
t.forEach(tag => ids.push(tag.id))
const onTagsChange = 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 (
<>
<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>
<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>}
<Tags tags={props.ar.tags} onChange={onTagsChange}/>
</Card>
</DrawerContent>
</DrawerOverlay>

@ -43,7 +43,7 @@ function PageContainer1(props: PageContainerProps) {
/>
<Flex px={[0,0,16,16]}>
<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}
</Box>
</Flex>

@ -22,6 +22,7 @@ import { Post } from "src/types/posts"
import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request"
import UnicornLike from "components/posts/unicorn-like"
import SvgButton from "components/svg-button"
const PostPage = () => {
const router = useRouter()
@ -35,6 +36,15 @@ const PostPage = () => {
}
}, [id])
useEffect(() => {
if (router && router.asPath.indexOf("#comments") > -1) {
setTimeout(() => {
location.href = "#comments"
},100)
}
},[router])
const getData = async () => {
const res = await requestApi.get(`/post/${id}`)
setPost(res.data)
@ -58,7 +68,6 @@ const PostPage = () => {
const getComments = async (id) => {
const res = await requestApi.get(`/story/comments/${id}`)
console.log(res.data)
setComments(res.data)
}
@ -96,7 +105,7 @@ const PostPage = () => {
{/* </HStack> */}
</Box>
<Box>
<IconButton
<SvgButton
mt="6"
aria-label="go to github"
variant="ghost"
@ -104,28 +113,30 @@ const PostPage = () => {
_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>}
icon="bookmark"
onClick={null}
/>
<Box mt="4">
<IconButton
<SvgButton
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>}
icon="share"
onClick={() => location.href="#comments"}
/>
</Box>
{post.creatorId === session?.user.id && <Box mt="4">
<IconButton
<SvgButton
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontWeight="300"
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>

@ -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 Container from "components/container"
import SEO from "components/seo"
@ -10,23 +10,38 @@ import PageContainer from "layouts/page-container"
import PageContainer1 from "layouts/page-container1"
import { useRouter } from "next/router"
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 { User } from "src/types/session"
import { requestApi } from "utils/axios/request"
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 router = useRouter()
const username = router.query.username
const session = useSession()
const [user, setUser]: [User, any] = useState(null)
const [posts, setPosts]: [Post[], any] = useState([])
const borderColor = useColorModeValue('white', 'transparent')
useEffect(() => {
if (username) {
requestApi.get(`/user/info/${username}`).then(res => setUser(res.data))
initData(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 (
<>
<SEO
@ -43,15 +58,15 @@ const UserPage = () => {
<Image src={user.avatar} height="130px" borderRadius="50%" border={`4px solid ${borderColor}`} />
<Heading fontSize="1.8rem">{user.nickname}</Heading>
{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 ml="5"><FaStar /></chakra.span><chakra.span ml="1">Following <chakra.a fontWeight="600">0</chakra.a></chakra.span>
</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>
</VStack>
</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" }}>
<Card>
<Text layerStyle="textSecondary">{user.about}</Text>
@ -86,7 +101,44 @@ const UserPage = () => {
<Text mt="2">{user.availFor}</Text>
</Box>}
</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>
{
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>
</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 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 siteConfig from "configs/site-config"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import React from "react"
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>
</>
)
import PageContainer1 from "layouts/page-container1"
import React, { useEffect, useState } from "react"
import { PostFilter } from "src/types/posts"
import { requestApi } from "utils/axios/request"
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 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 useSession from "hooks/use-session"
import { config } from "utils/config"
import Tags from "components/tags/tags"
var validator = require('validator');
const UserProfilePage = () => {
const [user, setUser] = useState(null)
const [skills, setSkills] = useState([])
const [isLargerThan1280] = useMediaQuery("(min-width: 768px)")
useEffect(() => {
requestApi.get("/user/self").then(res => setUser(res.data))
@ -28,7 +30,7 @@ const UserProfilePage = () => {
const toast = useToast()
const submitUser = async (values, _) => {
await requestApi.post(`/user/update`,values)
await requestApi.post(`/user/update`, values)
setUser(values)
toast({
description: "更新成功",
@ -70,7 +72,7 @@ const UserProfilePage = () => {
}
function validateUrl(value, canBeEmpty=true) {
function validateUrl(value, canBeEmpty = true) {
let url = value?.trim()
let error
if (!canBeEmpty) {
@ -115,7 +117,7 @@ const UserProfilePage = () => {
<Card p={[2, 2, 6, 6]}>
<Layout spacing={isLargerThan1280 ? "8" : "6"} alignItems={isLargerThan1280 ? 'top' : 'left'}>
<Box width="100%">
<VStack alignItems="left" spacing="6">
<VStack alignItems="left" spacing="6">
<Heading fontSize="1.2rem"></Heading>
<Field name="nickname" validate={validateNickname}>
{({ field, form }) => (
@ -199,6 +201,26 @@ const UserProfilePage = () => {
</FormControl>
)}
</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>
</Box>
<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 Container from "components/container"
import { MarkdownRender } from "components/markdown-editor/render"
import Posts from "components/posts/posts"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import useSession from "hooks/use-session"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import PageContainer1 from "layouts/page-container1"
import { useRouter } from "next/router"
import React, { useEffect, useState } from "react"
import { ReserveUrls } from "src/data/reserve-urls"
import { Post } from "src/types/posts"
import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request"
import { isAdmin } from "utils/role"
@ -17,14 +20,19 @@ import { isAdmin } from "utils/role"
const UserPage = () => {
const router = useRouter()
const [posts, setPosts]: [Post[], any] = useState([])
const [tag, setTag]: [Tag, any] = useState({})
const getTag = async () => {
const res = await requestApi.get(`/tag/${router.query.name}`)
const initData = async () => {
const res = await requestApi.get(`/tag/info/${router.query.name}`)
setTag(res.data)
const res1 = await requestApi.get(`/tag/posts/${res.data.id}`)
setPosts(res1.data)
}
useEffect(() => {
if (router.query.name) {
getTag()
initData()
}
}, [router.query.name])
@ -36,47 +44,61 @@ const UserPage = () => {
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer>
{tag.name && <HStack alignItems="top" spacing="4">
<Box width="70%">
<Card p="0">
<Image src={tag.cover} />
<Image src={tag.icon} width="80px" position="relative" top="-40px" left="40px"/>
<Flex justifyContent="space-between" alignItems="center" px="8" pb="6" mt="-1rem">
<Box>
<Heading size="lg">{tag.title}</Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">#{tag.name}</Text>
</Box>
<Box>
<Button colorScheme="teal">Follow</Button>
{isAdmin(session.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>}
</Box>
</Flex>
<PageContainer1>
{tag.name &&
<HStack alignItems="top" spacing="4" p="2">
<VStack width={["100%","100%","70%","70%"]} alignItems="left" spacing="2">
<Card p="0">
<Image src={tag.cover} />
<Image src={tag.icon} width="80px" position="relative" top="-40px" left="40px" />
<Flex justifyContent="space-between" alignItems="center" px="8" pb="6" mt="-1rem">
<Box>
<Heading size="lg">{tag.title}</Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">#{tag.name}</Text>
</Box>
<Box>
<Button colorScheme="teal">Follow</Button>
{isAdmin(session?.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>}
</Box>
</Flex>
</Card>
</Box>
<Box width="30%">
<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>
</Card>
{
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>
}
</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>
<Heading size="lg">{tag.postCount}</Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Posts</Text>
</Box>
</Flex>
</Card>
<Box>
<Heading size="lg">{tag.postCount}</Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Posts</Text>
</Box>
</Flex>
</Card>
<Card mt="4">
<Heading size="sm">About this tag</Heading>
<Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box>
</Card>
</Box>
</HStack>}
</PageContainer>
<Card mt="4">
<Heading size="sm">About this tag</Heading>
<Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box>
</Card>
</VStack>
</HStack>}
</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 (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/story"
@ -83,3 +84,60 @@ func LikeStory(c *gin.Context) {
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.GET("/story/comments/:id", api.GetStoryComments)
r.POST("/story/comment", IsLogin(), api.SubmitComment)
r.DELETE("/comment/:id", IsLogin(), api.DeleteComment)
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.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("/user/self", IsLogin(), api.GetUserSelf)
r.GET("/user/info/:username", api.GetUser)
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)
err := router.Run(config.Data.Server.Addr)
if err != nil {

@ -38,10 +38,20 @@ var sqlTables = map[string]string{
weibo VARCHAR(255),
facebook VARCHAR(255),
stackoverflow VARCHAR(255),
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 (
sid VARCHAR(255) primary key,
user_id INTEGER
@ -49,19 +59,20 @@ var sqlTables = map[string]string{
`,
"posts": `CREATE TABLE IF NOT EXISTS posts (
id VARCHAR(255) PRIMARY KEY,
creator INTEGER NOT NULL,
slug VARCHAR(64) NOT NULL,
title VARCHAR(255) NOT NULL,
md TEXT,
url VARCHAR(255),
cover VARCHAR(255),
brief TEXT,
likes INTEGER DEFAULT 0,
views INTEGER DEFAULT 0,
status tinyint NOT NULL,
created DATETIME NOT NULL,
updated DATETIME
id VARCHAR(255) PRIMARY KEY,
creator INTEGER NOT NULL,
slug VARCHAR(64) NOT NULL,
title VARCHAR(255) NOT NULL,
md TEXT,
url VARCHAR(255),
cover VARCHAR(255),
brief TEXT,
likes INTEGER DEFAULT 0,
views INTEGER DEFAULT 0,
comments INTEGER DEFAULT 0,
status tinyint NOT NULL,
created DATETIME NOT NULL,
updated DATETIME
);
CREATE INDEX IF NOT EXISTS posts_creator
ON posts (creator);
@ -124,4 +135,10 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS 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 (
"database/sql"
"errors"
"net/http"
"sort"
"time"
@ -23,6 +24,36 @@ func AddComment(c *models.Comment) *e.Error {
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
}
@ -89,8 +120,34 @@ func GetComment(id string) (*models.Comment, *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
_, 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 {
logger.Warn("delete comment replies error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
@ -119,3 +176,36 @@ func commentExist(id string) bool {
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) {
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)
if err != nil {
if err == sql.ErrNoRows {
return ars, e.New(http.StatusNotFound, e.NotFound)
}
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
@ -42,6 +39,55 @@ func UserPosts(uid int64) (models.Posts, *e.Error) {
}
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)
}
@ -124,10 +170,6 @@ 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)
@ -153,6 +195,12 @@ func DeletePost(id string) *e.Error {
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
}
@ -218,6 +266,35 @@ func GetPostCreator(id string) (int64, *e.Error) {
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 {
var nid string
err := db.Conn.QueryRow("SELECT id from posts WHERE id=?", id).Scan(&nid)

@ -7,6 +7,7 @@ import (
"time"
"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/e"
"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
}
// 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
}
@ -86,5 +107,18 @@ func UpdateUser(u *models.User) *e.Error {
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
}

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

@ -14,12 +14,13 @@ type User struct {
Email string `json:"email"`
Role RoleType `json:"role"`
Tagline string `json:"tagline"`
Cover string `json:"cover"`
Location string `json:"location"`
AvailFor string `json:"availFor"`
About string `json:"about"`
Skills string `json:"skills"`
Tagline string `json:"tagline"`
Cover string `json:"cover"`
Location string `json:"location"`
AvailFor string `json:"availFor"`
About string `json:"about"`
RawSkills []*Tag `json:"rawSkills"`
Skills []int64 `json:"skills"`
Website string `json:"website"`
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 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 {
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)

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

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

@ -1,5 +1,5 @@
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 { ReserveUrls } from "src/data/reserve-urls"
import NextLink from "next/link"
@ -10,25 +10,24 @@ import Link from "next/link"
import { useRouter } from "next/router"
type Props = PropsOf<typeof chakra.div> & {
size?: 'lg' | 'md'
post : Post
showFooter?: boolean
}
export const PostAuthor= ({post}:Props) =>{
export const PostAuthor= ({post,showFooter=true,size='lg'}: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"/>
<Avatar src={post.creator.avatar} size={size} 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">
<Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600" ml="1">{moment(post.created).fromNow()}</chakra.span></Text>
{showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3">
<FaGithub /> <chakra.span>4 min read</chakra.span>
</HStack>
</HStack>}
</VStack>
</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[]
selected: Tag[]
onChange: any
size?: 'lg' | 'md'
}
@ -44,7 +45,7 @@ export const TagInput = (props: Props) => {
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}>
<PopoverTrigger><Box width="100%"></Box></PopoverTrigger>
<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 { Tag } from './tag';
export enum PostFilter {
Best = "best",
Featured = "featured",
Recent = "recent"
}
export interface Post {
id?: string
slug?: string
@ -14,7 +20,7 @@ export interface Post {
created?: string
tags?: number[]
rawTags?: Tag[]
likes? : number
likes? : number
liked? : boolean
recommands? : number
comments? : number
}

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

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

Loading…
Cancel
Save