add tags for user home

pull/50/head
sunface 4 years ago
parent e0c35fae98
commit a329949f26

@ -32,7 +32,7 @@ function PostNav(props:Props) {
useEffect(() => {
if (post) {
requestApi.get(`/interaction/followed/${post.id}`).then(res => setFollowed(res.data))
requestApi.get(`/interaction/followed/${post.creator.id}`).then(res => setFollowed(res.data))
}
}, [])
@ -50,7 +50,7 @@ function PostNav(props:Props) {
<Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}>
<HStack spacing="2">
<Heading size="md">Sunface</Heading>
{followed !== null && <Follow targetID={post?.id} followed={followed} />}
{followed !== null && <Follow targetID={post?.creator.id} followed={followed} />}
</HStack>

@ -22,27 +22,61 @@ import Posts from "components/story/posts"
import Link from "next/link"
import Empty from "components/empty"
import Count from "components/count"
import { Tag } from "src/types/tag"
const UserPage = () => {
const router = useRouter()
const username = router.query.username
const session = useSession()
const [user, setUser]: [User, any] = useState(null)
const [rawPosts, setRawPosts]: [Post[], any] = useState([])
const [posts, setPosts]: [Post[], any] = useState([])
const [tags,setTags]:[Tag[],any] = useState([])
const [tagFilter,setTagFilter]:[Tag,any] = useState(null)
const borderColor = useColorModeValue('white', 'transparent')
useEffect(() => {
if (username) {
initData(username)
}
}, [username])
const initData = async (username) => {
const res = await requestApi.get(`/user/info/${username}`)
setUser(res.data)
getTags(res.data.id)
const res1 = await requestApi.get(`/user/posts/${res.data.id}`)
setPosts(res1.data)
setRawPosts(res1.data)
}
const getTags = async (userID) => {
const res = await requestApi.get(`/tag/user/${userID}`)
setTags(res.data)
}
const filterPostsByTag = tag => {
if (tag.id === tagFilter?.id) {
setTagFilter(null)
setPosts(rawPosts)
return
}
setTagFilter(tag)
const p = []
rawPosts.forEach(post => {
for (let i=0;i<post.rawTags.length;i++) {
if (post.rawTags[i].id === tag.id) {
p.push(post)
break
}
}
})
setPosts(p)
}
return (
<>
@ -61,7 +95,7 @@ const UserPage = () => {
<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="1" alignItems="center">
<chakra.span><FaHeart /></chakra.span><chakra.span ml="1">Followers <chakra.a fontWeight="600"><Count count={12312312312}/></chakra.a></chakra.span>
<chakra.span><FaHeart /></chakra.span><chakra.span ml="1">Followers <chakra.a fontWeight="600"><Count count={user.follows}/></chakra.a></chakra.span>
<chakra.span ml="5"><FaStar /></chakra.span><chakra.span ml="1">Following <chakra.a fontWeight="600"><Count count={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>}><chakra.span display={{base:"none",md:"block"}}>Edit Profile</chakra.span></Button>
@ -91,7 +125,7 @@ const UserPage = () => {
<chakra.span fontWeight="500" ml="2">{moment(user.created).fromNow()}</chakra.span>
</HStack>
<HStack layerStyle="textSecondary" fontSize="1.4rem" mt="4" spacing="5">
<HStack layerStyle="textSecondary" fontSize="1.4rem" mt={(user.github || user.twitter || user.facebook || user.stackoverflow || user.weibo || user.zhihu) ? 4 : 0 } spacing="5">
{user.github && <chakra.a href={user.github} target="_blank"><FaGithub /></chakra.a>}
{user.twitter && <chakra.a href={user.twitter} target="_blank"><FaTwitter /></chakra.a>}
{user.facebook && <chakra.a href={user.facebook} target="_blank"><FaFacebook /></chakra.a>}
@ -107,7 +141,7 @@ const UserPage = () => {
</Box>}
</Card>
{user.rawSkills.length > 0 && <Card>
<Heading size="md" layerStyle="textSecondary" fontWeight="500">My Tech Stack</Heading>
<Heading size="md" fontSize="1.2rem" layerStyle="textSecondary" fontWeight="500"></Heading>
<Wrap mt="4" p="1">
{
user.rawSkills.map(skill =>
@ -120,13 +154,19 @@ const UserPage = () => {
}
</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> */}
{tags.length > 0 && <Card>
<Heading size="md" fontSize="1.2rem" layerStyle="textSecondary" fontWeight="500"></Heading>
<Wrap mt="4" p="1">
{
tags.map(tag =>
<Button size="sm" variant="ghost" p="0" onClick={() => filterPostsByTag(tag)} _focus={null}>
<Box className={tagFilter?.id === tag.id ? "tag-bg" : null} py="2" px="3">{tag.name} &nbsp; {tag.posts}</Box>
</Button>
)
}
</Wrap>
</Card>}
</VStack>
@ -138,7 +178,7 @@ const UserPage = () => {
</Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} />
<Posts posts={posts} showFooter={tagFilter === null}/>
</Card>
}
</Box>

@ -29,7 +29,7 @@ import Empty from "components/empty"
const BookmarksPage = () => {
const [filter, setFilter]:[Tag,any] = useState({id:"-1"})
const [filter, setFilter]:[Tag,any] = useState(null)
const [tags, setTags]: [Tag[], any] = useState([])
const [rawPosts,setRawPosts]: [Post[],any] = useState([])
const [posts,setPosts]: [Post[],any] = useState([])
@ -38,41 +38,44 @@ import Empty from "components/empty"
getBookmarkPosts()
}, [])
useEffect(() => {
filterPosts()
}, [filter])
const getBookmarkPosts = async() => {
const res = await requestApi.get(`/story/bookmark/posts`)
setRawPosts(res.data)
setPosts(res.data)
const ts = [{id:-1,title:'All Tags',icon: 'https://cdn.hashnode.com/res/hashnode/image/upload/v1605105898259/3vuMFM8qM.png?w=200&h=200&fit=crop&crop=entropy&auto=compress&auto=compress'}]
const ts = []
res.data.forEach(post => {
post.rawTags?.forEach(tag => {
if (!find(ts, t => t.id === tag.id)) {
ts.push(tag)
}
})
for (let i=0;i<post.rawTags.length;i++) {
const tag = post.rawTags[i]
if (!find(ts, t => t.id === tag.id)) {
ts.push(tag)
break
}
}
})
setTags(ts)
}
const filterPosts = () => {
if (filter.id === "-1") {
setPosts(rawPosts)
return
const filterPostsByTag = (t:Tag) => {
if (t.id === filter?.id) {
setPosts(rawPosts)
setFilter(null)
return
}
const newPosts = []
rawPosts.forEach(post => {
post.rawTags?.forEach(tag => {
if (tag.id === filter.id) {
newPosts.push(post)
}
})
for (let i=0;i<post.rawTags.length;i++) {
if (t.id === post.rawTags[i].id) {
newPosts.push(post)
break
}
}
})
setPosts(newPosts)
setFilter(t)
}
return (
@ -95,7 +98,7 @@ import Empty from "components/empty"
<Wrap pt="4" pb="1" pl="4" alignItems="center">
{
tags.map(t =>
<HStack px="2" py="1" spacing="1" mr="3" cursor="pointer" key={t.id} className={t.id===filter.id ?"tag-bg": null} onClick={() => setFilter(t)}>
<HStack px="2" py="1" spacing="1" mr="3" cursor="pointer" key={t.id} className={t.id===filter?.id ?"tag-bg": null} onClick={() => filterPostsByTag(t)}>
<Image src={t.icon} width="30px" height="30px" className="bordered"/>
<Text fontSize=".9rem">{t.title}</Text>
</HStack>)

@ -201,15 +201,6 @@ 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 >
@ -218,9 +209,6 @@ const UserProfilePage = () => {
</FormControl>
)}
</Field>
<Box>
</Box>
</VStack>
</Box>
<Box width="100%" >

@ -18,6 +18,7 @@ import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request"
import { isAdmin } from "utils/role"
import Follow from "components/interaction/follow"
import Count from "components/count"
const UserPage = () => {
const router = useRouter()
@ -88,12 +89,12 @@ const UserPage = () => {
<Card>
<Flex justifyContent="space-between" alignItems="center" px={[0, 2, 4, 8]}>
<Box>
<Heading size="lg">59.8K</Heading>
<Heading size="lg"><Count count={tag.follows}/></Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Followers</Text>
</Box>
<Box>
<Heading size="lg">{tag.postCount}</Heading>
<Heading size="lg"><Count count={tag.posts} /></Heading>
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Posts</Text>
</Box>
</Flex>

@ -77,3 +77,14 @@ func DeleteTag(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func GetUserTags(c *gin.Context) {
userID := c.Param("userID")
res, err := tags.GetUserTags(userID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(res))
}

@ -62,7 +62,7 @@ func (s *Server) Start() error {
r.GET("/tag/all", api.GetTags)
r.GET("/tag/posts/:id", api.GetTagPosts)
r.GET("/tag/info/:name", api.GetTag)
r.GET("/tag/user/:userID", api.GetUserTags)
// user apis
r.GET("/user/all", api.GetUsers)
r.GET("/user/self", IsLogin(), api.GetUserSelf)

@ -22,13 +22,13 @@ func Init() error {
}
// check whether tables have been created
var id int64
err = db.Conn.QueryRow("select id from user where id=?", 1).Scan(&id)
var id string
err = db.Conn.QueryRow("select id from user where username=?", config.Data.User.SuperAdminUsername).Scan(&id)
if err != nil && !strings.Contains(err.Error(), "no such table") && err != sql.ErrNoRows {
return err
}
if id != 1 {
if err == sql.ErrNoRows {
log.RootLogger.Info("Database tables have not been created, start creating")
err = initTables()
if err != nil {

@ -123,7 +123,8 @@ var sqlTables = map[string]string{
"tags_using": `CREATE TABLE IF NOT EXISTS tags_using (
tag_id VARCHAR(255),
target_type VARCHAR(1),
target_id VARCHAR(255)
target_id VARCHAR(255),
target_creator VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS tags_using_tagid
ON tags_using (tag_id);

@ -95,7 +95,7 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
}
//update tags
err = tags.UpdateTargetTags(post.ID, post.Tags)
err = tags.UpdateTargetTags(user.ID, post.ID, post.Tags)
if err != nil {
logger.Warn("upate tags error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)

@ -130,6 +130,12 @@ func getPosts(user *models.User, rows *sql.Rows) models.Posts {
}
ar.Likes = interaction.GetLikes(ar.ID)
_, rawTags, err := tags.GetTargetTags(ar.ID)
if err != nil {
logger.Warn("get tags error", "error", err)
}
ar.RawTags = rawTags
posts = append(posts, ar)
}

@ -8,6 +8,7 @@ import (
"time"
"github.com/asaskevich/govalidator"
"github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/log"
@ -70,7 +71,7 @@ func SubmitTag(tag *models.Tag) *e.Error {
func GetTags() (models.Tags, *e.Error) {
tags := make(models.Tags, 0)
rows, err := db.Conn.Query("SELECT id,creator,title,md,name,icon,cover,created,updated from tags")
rows, err := db.Conn.Query("SELECT id,creator,title,md,name,icon,cover from tags")
if err != nil {
if err == sql.ErrNoRows {
return tags, nil
@ -82,7 +83,7 @@ func GetTags() (models.Tags, *e.Error) {
for rows.Next() {
var rawMd []byte
tag := &models.Tag{}
err := rows.Scan(&tag.ID, &tag.Creator, &tag.Title, &rawMd, &tag.Name, &tag.Icon, &tag.Cover, &tag.Created, &tag.Updated)
err := rows.Scan(&tag.ID, &tag.Creator, &tag.Title, &rawMd, &tag.Name, &tag.Icon, &tag.Cover)
if err != nil {
logger.Warn("scan tags error", "error", err)
continue
@ -94,7 +95,7 @@ func GetTags() (models.Tags, *e.Error) {
tag.SetCover()
tags = append(tags, tag)
db.Conn.QueryRow("SELECT count(*) FROM tags_using WHERE tag_id=?", tag.ID).Scan(&tag.PostCount)
db.Conn.QueryRow("SELECT count(*) FROM tags_using WHERE tag_id=? and target_type != ?", tag.ID, models.IDTypeUser).Scan(&tag.Posts)
}
sort.Sort(tags)
@ -115,8 +116,8 @@ func DeleteTag(id int64) *e.Error {
func GetTag(id string, name string) (*models.Tag, *e.Error) {
tag := &models.Tag{}
var rawmd []byte
err := db.Conn.QueryRow("SELECT id,creator,title,name,icon,cover,created,updated,md from tags where id=? or name=?", id, name).Scan(
&tag.ID, &tag.Creator, &tag.Title, &tag.Name, &tag.Icon, &tag.Cover, &tag.Created, &tag.Updated, &rawmd,
err := db.Conn.QueryRow("SELECT id,creator,title,name,icon,cover,md from tags where id=? or name=?", id, name).Scan(
&tag.ID, &tag.Creator, &tag.Title, &tag.Name, &tag.Icon, &tag.Cover, &rawmd,
)
if err != nil {
if err == sql.ErrNoRows {
@ -129,9 +130,11 @@ func GetTag(id string, name string) (*models.Tag, *e.Error) {
md, _ := utils.Uncompress(rawmd)
tag.Md = string(md)
db.Conn.QueryRow("SELECT count(*) FROM tags_using WHERE tag_id=?", tag.ID).Scan(&tag.PostCount)
db.Conn.QueryRow("SELECT count(*) FROM tags_using WHERE tag_id=? and target_type !=?", tag.ID, models.IDTypeUser).Scan(&tag.Posts)
tag.SetCover()
tag.Follows = interaction.GetFollows(tag.ID)
return tag, nil
}
@ -173,14 +176,14 @@ func GetTargetTags(targetID string) ([]string, []*models.Tag, error) {
return ids, rawTags, nil
}
func UpdateTargetTags(targetID string, tags []string) error {
func UpdateTargetTags(targetCreator string, targetID string, tags []string) error {
_, err := db.Conn.Exec("DELETE FROM tags_using WHERE target_id=?", targetID)
if err != nil {
return err
}
for _, tag := range tags {
_, err = db.Conn.Exec("INSERT INTO tags_using (tag_id,target_type,target_id) VALUES (?,?,?)", tag, models.GetIDType(targetID), targetID)
_, err = db.Conn.Exec("INSERT INTO tags_using (tag_id,target_type,target_id,target_creator) VALUES (?,?,?,?)", tag, models.GetIDType(targetID), targetID, targetCreator)
if err != nil {
logger.Warn("add post tag error", "error", err)
}
@ -219,3 +222,39 @@ func GetTargetIDs(tagID string) ([]string, error) {
return ids, nil
}
func GetUserTags(userID string) ([]*models.Tag, *e.Error) {
tagsMap := make(map[string]*models.Tag)
rows, err := db.Conn.Query("SELECT tag_id from tags_using where target_creator=?", userID)
if err != nil {
logger.Warn("get tag error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
var tagID string
for rows.Next() {
rows.Scan(&tagID)
tag, ok := tagsMap[tagID]
if !ok {
tagsMap[tagID] = &models.Tag{ID: tagID, Posts: 1}
} else {
tag.Posts = tag.Posts + 1
}
}
tags := make(models.Tags, 0)
for _, t := range tagsMap {
tag, err := GetSimpleTag(t.ID, "")
if err != nil {
logger.Warn("get simple tag error", "error", err)
continue
}
tag.Posts = t.Posts
tags = append(tags, tag)
}
sort.Sort(tags)
return tags, nil
}

@ -7,6 +7,7 @@ import (
"time"
"github.com/imdotdev/im.dev/server/internal/cache"
"github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/internal/tags"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
@ -62,6 +63,8 @@ func GetUserDetail(id string, username string) (*models.User, *e.Error) {
user.RawSkills = rawSkills
user.Skills = skills
user.Follows = interaction.GetFollows(user.ID)
return user, nil
}
@ -75,7 +78,7 @@ func UpdateUser(u *models.User) *e.Error {
return e.New(http.StatusInternalServerError, e.Internal)
}
var nid int64
var nid string
err = db.Conn.QueryRow("SELECT id FROM user_profile WHERE id=?", u.ID).Scan(&nid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("update user profile error", "error", err)
@ -97,7 +100,7 @@ func UpdateUser(u *models.User) *e.Error {
}
//update user skills
err = tags.UpdateTargetTags(u.ID, u.Skills)
err = tags.UpdateTargetTags("", u.ID, u.Skills)
if err != nil {
logger.Warn("upate tags error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)

@ -1,18 +1,17 @@
package models
import "time"
type Tag struct {
ID string `json:"id"`
Creator string `json:"creator,omitempty"`
Title string `json:"title"`
Name string `json:"name,omitempty"`
Md string `json:"md,omitempty"`
Cover string `json:"cover,omitempty"`
Icon string `json:"icon"`
PostCount int `json:"postCount,omitempty"`
Created time.Time `json:"created,omitempty"`
Updated time.Time `json:"updated,omitempty"`
ID string `json:"id"`
Creator string `json:"creator,omitempty"`
Title string `json:"title"`
Name string `json:"name,omitempty"`
Md string `json:"md,omitempty"`
Cover string `json:"cover,omitempty"`
Icon string `json:"icon"`
Posts int `json:"posts"`
Follows int `json:"follows"`
// Created time.Time `json:"created,omitempty"`
// Updated time.Time `json:"updated,omitempty"`
}
func (t *Tag) SetCover() {
@ -26,5 +25,5 @@ type Tags []*Tag
func (t Tags) Len() int { return len(t) }
func (t Tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Tags) Less(i, j int) bool {
return t[i].Created.Unix() > t[j].Created.Unix()
return t[i].Posts > t[j].Posts
}

@ -30,6 +30,8 @@ type User struct {
Facebook string `json:"facebook"`
Stackoverflow string `json:"stackoverflow"`
Follows int `json:"follows"`
LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
Created time.Time `json:"created"`
}

@ -18,7 +18,7 @@ export const TagListCard= (props:Props) =>{
<Heading size="sm" display="flex" alignItems="center" cursor="pointer">
{tag.title}
</Heading>
<Text layerStyle="textSecondary" fontSize=".9rem" mt="1" fontWeight="450">{tag.postCount} posts</Text>
<Text layerStyle="textSecondary" fontSize=".9rem" mt="1" fontWeight="450">{tag.posts} posts</Text>
</Box>
<Image src={tag.icon} width="35px" />
</Flex>

@ -4,6 +4,7 @@ import { Tag } from "src/types/tag"
import { ReserveUrls } from "src/data/reserve-urls"
import NextLink from "next/link"
import userCustomTheme from "theme/user-custom"
import Count from "components/count"
type Props = PropsOf<typeof chakra.div> & {
tag: Tag
@ -31,7 +32,7 @@ export const TagCard= (props:Props) =>{
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
<Button size="sm" onClick={onDelete} variant="ghost">Delete</Button>
</HStack> :
<ChakraTag py="1" px="3" colorScheme="cyan">{tag.postCount} posts</ChakraTag>
<ChakraTag py="1" px="3" colorScheme="cyan"><Count count={tag.posts} />&nbsp;posts</ChakraTag>
}
</Flex>
)

@ -40,7 +40,7 @@ export const UserMenu = () => {
icon={session.user.avatar !== '' ? <Image
boxSize="2.8em"
borderRadius="full"
src="https://placekitten.com/100/100"
src={session.user.avatar}
alt="user"
/> :
<FaUserAlt />

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

@ -7,7 +7,7 @@ export interface Session {
export interface User {
// basic info
id: number
id: string
username: string
nickname: string
avatar: string
@ -32,12 +32,14 @@ export interface User {
facebook?: string
stackoverflow?: string
follows?: number
lastSeenAt?: string
created?: string
}
export interface UserSimple {
id: number
id: string
username: string
nickname: string
avatar: string

@ -23,7 +23,6 @@ const customTheme = extendTheme({
},
styles: {
global: (props) => {
console.log(props)
return ({
'.hover-bg:hover': {
background: mode(userCustomTheme.hoverBg.light,userCustomTheme.hoverBg.dark )(props),

Loading…
Cancel
Save