Added following other users.

This commit is contained in:
Martin Sosic 2020-11-27 16:10:02 +01:00
parent cdb7484279
commit 804e3ef37d
8 changed files with 288 additions and 13 deletions

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory } from 'react-router-dom'
import moment from 'moment'
import useAuth from '@wasp/auth/useAuth.js' import useAuth from '@wasp/auth/useAuth.js'
import logout from '@wasp/auth/logout.js' import logout from '@wasp/auth/logout.js'
@ -8,6 +9,7 @@ import getUser from '@wasp/queries/getUser'
import getArticlesByUser from '@wasp/queries/getArticlesByUser' import getArticlesByUser from '@wasp/queries/getArticlesByUser'
import getFavoritedArticles from '@wasp/queries/getFavoritedArticles' import getFavoritedArticles from '@wasp/queries/getFavoritedArticles'
import setArticleFavorited from '@wasp/actions/setArticleFavorited' import setArticleFavorited from '@wasp/actions/setArticleFavorited'
import followUser from '@wasp/actions/followUser'
import { useQuery } from '@wasp/queries' import { useQuery } from '@wasp/queries'
import Navbar from './Navbar' import Navbar from './Navbar'
@ -16,6 +18,8 @@ import smileyImageUrl from './smiley.jpg'
const UserProfilePage = (props) => { const UserProfilePage = (props) => {
const history = useHistory() const history = useHistory()
const { data: me } = useAuth()
const username = props.match.params.username const username = props.match.params.username
const { data: user, error: userError } = useQuery(getUser, { username }) const { data: user, error: userError } = useQuery(getUser, { username })
@ -28,9 +32,6 @@ const UserProfilePage = (props) => {
history.push("/") history.push("/")
} }
// TODO: List My Articles
// TODO: List Favorited Articles
return user ? ( return user ? (
<div> <div>
<Navbar /> <Navbar />
@ -38,26 +39,52 @@ const UserProfilePage = (props) => {
<img src={user.profilePictureUrl || smileyImageUrl} /> <img src={user.profilePictureUrl || smileyImageUrl} />
<p> { user.username } </p> <p> { user.username } </p>
<p> { user.bio } </p> <p> { user.bio } </p>
<div> { me && me.username === username && (
{ /* TODO: Show this link only if user is logged in. */ } <div>
<Link to='/settings'>Edit Profile Settings</Link> <Link to='/settings'>Edit Profile Settings</Link>
</div> </div>
)}
{ me && me.username !== username && (
<div>
<FollowUserButton user={user} />
</div>
)}
<Articles user={user} /> <Articles user={user} />
</div> </div>
) : null ) : null
} }
const FollowUserButton = (props) => {
const user = props.user
const { data: me } = useAuth()
const toggleFollow = async () => {
try {
followUser({ username: user.username, follow: !user.following })
} catch (err) {
console.error(err)
window.alert(err)
}
}
return me && me.username !== user.username ? (
<button onClick={toggleFollow}>
{ user.following ? 'Unfollow' : 'Follow' }
</button>
) : null
}
const Articles = (props) => { const Articles = (props) => {
const user = props.user const user = props.user
const { data: myArticles } = useQuery(getArticlesByUser, { username: props.user.username }) const { data: authoredArticles } = useQuery(getArticlesByUser, { username: props.user.username })
const { data: favoritedArticles } = useQuery(getFavoritedArticles, { username: props.user.username }) const { data: favoritedArticles } = useQuery(getFavoritedArticles, { username: props.user.username })
return ( return (
<div> <div>
<h1> My Articles </h1> <h1> My Articles </h1>
<ArticleList articles={myArticles} /> <ArticleList articles={authoredArticles} />
<h1> Favorited Articles </h1> <h1> Favorited Articles </h1>
<ArticleList articles={favoritedArticles} /> <ArticleList articles={favoritedArticles} />
</div> </div>
@ -82,11 +109,20 @@ const Article = (props) => {
} }
return ( return (
<div> <div style={{ border: '1px solid black' }}>
<Link to={`/article/${article.slug}`}> <Link to={`/article/${article.slug}`}>
<h2> { article.title } </h2> <h2> { article.title } </h2>
</Link> </Link>
<p> { article.description } </p> <p> { article.description } </p>
<p>
<em> Tags: </em>
{ article.tags.map(t => t.name).join('; ') }
</p>
<p>
<img src={ article.user.profilePictureUrl || smileyImageUrl } width='30px' />
<div> { article.user.username } </div>
<div> { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
</p>
<div> <div>
<button onClick={toggleArticleFavorited}> <button onClick={toggleArticleFavorited}>
{ article.favorited ? 'Unlike' : 'Like' } ({ article.favoritesCount }) { article.favorited ? 'Unlike' : 'Like' } ({ article.favoritesCount })

View File

@ -4,7 +4,6 @@ import slug from 'slug'
export const signup = async ({ username, email, password }, context) => { export const signup = async ({ username, email, password }, context) => {
try { try {
console.log('juhu')
await createNewUser({ username, email, password }) await createNewUser({ username, email, password })
} catch (err) { } catch (err) {
// TODO: I wish I didn't have to do this, I would love this to be in some // TODO: I wish I didn't have to do this, I would love this to be in some
@ -114,6 +113,22 @@ export const setArticleFavorited = async ({ id, favorited }, context) => {
}) })
} }
export const followUser = async ({ username, follow }, context) => {
if (!context.user) { throw new HttpError(403) }
await context.entities.User.update({
where: { username },
data: {
followedBy: {
...(follow === true ? { connect: { id: context.user.id } } :
follow === false ? { disconnect: { id: context.user.id } } :
{}
)
}
}
})
}
export const createComment = async ({ articleId, content }, context) => { export const createComment = async ({ articleId, content }, context) => {
if (!context.user) { throw new HttpError(403) } if (!context.user) { throw new HttpError(403) }

View File

@ -15,12 +15,24 @@ export const getUser = async ({ username }, context) => {
// TODO: Tricky, if you forget this you could return unwanted fields // TODO: Tricky, if you forget this you could return unwanted fields
// like hashed password! // like hashed password!
// It would be cool if we had some protection against making this mistake easily. // It would be cool if we had some protection against making this mistake easily.
select: userPublicSelection select: {
...userPublicSelection,
followedBy: { select: { id: true } }
}
}) })
if (!user) throw new HttpError(404, 'No user with username ' + username) if (!user) throw new HttpError(404, 'No user with username ' + username)
userSetFollowedFields(user, context.user)
return user return user
} }
const userSetFollowedFields = (user, me) => {
user.following = me && user.followedBy.find(({ id }) => id === me.id)
user.followersCount = user.followedBy.length
delete user.followedBy
}
// TODO: I extracted this articleInclude and articleSetFavoritedFields to enable // TODO: I extracted this articleInclude and articleSetFavoritedFields to enable
// reusing of logic that shapes articles as they come out of the server, // reusing of logic that shapes articles as they come out of the server,
// but I wonder if there is a more elegant way - here there are a lot of assumptions, // but I wonder if there is a more elegant way - here there are a lot of assumptions,

View File

@ -48,6 +48,8 @@ entity User {=psl
articles Article[] articles Article[]
comments Comment[] comments Comment[]
favoriteArticles Article[] @relation("FavoritedArticles") favoriteArticles Article[] @relation("FavoritedArticles")
followedBy User[] @relation("FollowedUser", references: [id])
following User[] @relation("FollowedUser", references: [id])
psl=} psl=}
entity Article {=psl entity Article {=psl
@ -103,6 +105,11 @@ query getUser {
entities: [User] entities: [User]
} }
action followUser {
fn: import { followUser } from "@ext/actions.js",
entities: [User]
}
query getArticlesByUser { query getArticlesByUser {
fn: import { getArticlesByUser } from "@ext/queries.js", fn: import { getArticlesByUser } from "@ext/queries.js",
entities: [Article] entities: [Article]

View File

@ -0,0 +1,48 @@
# Migration `20201127145135-added-following-of-other-users`
This migration has been generated by Martin Sosic at 11/27/2020, 3:51:35 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "_FollowedUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
)
CREATE UNIQUE INDEX "_FollowedUser_AB_unique" ON "_FollowedUser"("A", "B")
CREATE INDEX "_FollowedUser_B_index" ON "_FollowedUser"("B")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201127131647-init..20201127145135-added-following-of-other-users
--- datamodel.dml
+++ datamodel.dml
@@ -1,8 +1,8 @@
datasource db {
provider = "sqlite"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
@@ -19,8 +19,10 @@
articles Article[]
comments Comment[]
favoriteArticles Article[] @relation("FavoritedArticles")
+ followedBy User[] @relation("FollowedUser", references: [id])
+ following User[] @relation("FollowedUser", references: [id])
}
model Article {
id Int @id @default(autoincrement())
```

View File

@ -0,0 +1,59 @@
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
password String
bio String?
profilePictureUrl String?
articles Article[]
comments Comment[]
favoriteArticles Article[] @relation("FavoritedArticles")
followedBy User[] @relation("FollowedUser", references: [id])
following User[] @relation("FollowedUser", references: [id])
}
model Article {
id Int @id @default(autoincrement())
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
description String
markdownContent String
user User @relation(fields: [userId], references: [id])
userId Int
comments Comment[]
tags ArticleTag[]
favoritedBy User[] @relation("FavoritedArticles")
}
model Comment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
content String
user User @relation(fields: [userId], references: [id])
userId Int
article Article @relation(fields: [articleId], references: [id])
articleId Int
}
model ArticleTag {
name String @id
articles Article[]
}

View File

@ -0,0 +1,97 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateField",
"model": "User",
"field": "followedBy",
"type": "User",
"arity": "List"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "followedBy"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "followedBy"
},
"directive": "relation"
},
"argument": "",
"value": "\"FollowedUser\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "followedBy"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "User",
"field": "following",
"type": "User",
"arity": "List"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "following"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "following"
},
"directive": "relation"
},
"argument": "",
"value": "\"FollowedUser\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "following"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
}
]
}

View File

@ -1,3 +1,4 @@
# Prisma Migrate lockfile v1 # Prisma Migrate lockfile v1
20201127131647-init 20201127131647-init
20201127145135-added-following-of-other-users