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

@ -21,58 +21,27 @@ 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))
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))
const onTagsChange = ids => {
props.ar.tags = ids
}
@ -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"
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 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}
/>
<PageContainer>
<Card width="200px">
<chakra.h1>NOT FOUND</chakra.h1>
<p>You just hit a route that doesn&#39;t exist... the sadness.</p>
<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>
</PageContainer>
</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) {
@ -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,12 +44,13 @@ const UserPage = () => {
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer>
{tag.name && <HStack alignItems="top" spacing="4">
<Box width="70%">
<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"/>
<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>
@ -49,15 +58,28 @@ const UserPage = () => {
</Box>
<Box>
<Button colorScheme="teal">Follow</Button>
{isAdmin(session.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</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%">
{
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]}>
<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>
@ -74,9 +96,9 @@ const UserPage = () => {
<Heading size="sm">About this tag</Heading>
<Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box>
</Card>
</Box>
</VStack>
</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 (
"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 {

@ -42,6 +42,16 @@ var sqlTables = map[string]string{
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
@ -59,6 +69,7 @@ var sqlTables = map[string]string{
brief TEXT,
likes INTEGER DEFAULT 0,
views INTEGER DEFAULT 0,
comments INTEGER DEFAULT 0,
status tinyint NOT NULL,
created DATETIME NOT NULL,
updated DATETIME
@ -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"`

@ -19,7 +19,8 @@ type User struct {
Location string `json:"location"`
AvailFor string `json:"availFor"`
About string `json:"about"`
Skills string `json:"skills"`
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
@ -16,5 +22,5 @@ export interface Post {
rawTags?: Tag[]
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