mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-25 10:03:07 +03:00
Added following other users.
This commit is contained in:
parent
cdb7484279
commit
804e3ef37d
@ -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 })
|
||||||
|
@ -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) }
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
@ -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())
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -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[]
|
||||||
|
}
|
||||||
|
|
@ -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]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
# Prisma Migrate lockfile v1
|
# Prisma Migrate lockfile v1
|
||||||
|
|
||||||
20201127131647-init
|
20201127131647-init
|
||||||
|
20201127145135-added-following-of-other-users
|
Loading…
Reference in New Issue
Block a user