Skip to content

Commit

Permalink
refactor: extra ArticleMeta
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed May 20, 2024
1 parent 3686d1f commit b177aa1
Showing 1 changed file with 137 additions and 129 deletions.
266 changes: 137 additions & 129 deletions examples/realworld/src/ui/pages/article.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AsyncData, Fx, Link, RefArray, RefSubject, Router } from "@typed/core"
import type { EventWithTarget } from "@typed/dom/EventTarget"
import type { ArticleSlug, Comment } from "@typed/realworld/model"
import type { Article, ArticleSlug, ArticleTag, Comment } from "@typed/realworld/model"
import { CommentBody, Image } from "@typed/realworld/model"
import { Articles, Comments, CurrentUser, isAuthenticated, Profiles } from "@typed/realworld/services"
import { formatMonthAndDay, formatMonthDayYear } from "@typed/realworld/ui/common/date"
Expand All @@ -13,120 +13,26 @@ import * as Option from "effect/Option"
export const route = Routes.article
export type Params = Route.Route.Type<typeof route>

const FALLBACK_IMAGE = Image.make("https://api.realworld.io/images/demo-avatar.png")
const FALLBACK_IMAGE = Image.make(
"https://api.realworld.io/images/demo-avatar.png"
)

const renderTag = (tag: RefSubject.RefSubject<ArticleTag>) =>
html`<li class="tag-default tag-pill tag-outline">${tag}</li>`

const signInOrSignUp = html`<p show-authed="false" style="display: inherit;">
${Link({ to: "/login", relative: false }, "Sign in")} or
${Link({ to: "/register", relative: false }, "sign up")} to add comments on
this article.
</p>`

export const main = (params: RefSubject.RefSubject<Params>) =>
Fx.gen(function*(_) {
const ref = yield* _(RefSubject.make(RefSubject.mapEffect(params, Articles.get)))
const article = RefSubject.proxy(ref)
const author = RefSubject.proxy(article.author)
const authorProfileHref = RefSubject.map(author.username, (username) => Routes.profile.interpolate({ username }))
const authorImage = RefSubject.map(author.image, (img) => Option.getOrElse(img, () => FALLBACK_IMAGE))
const comments = yield* _(RefSubject.make(RefSubject.mapEffect(article.slug, Comments.get)))
const createdDate = RefSubject.map(article.createdAt, formatMonthDayYear)
const currentUserIsAuthor = RefSubject.map(
RefSubject.struct({
username: author.username,
currentUser: CurrentUser
}),
({ currentUser, username }) =>
AsyncData.isSuccess(currentUser) &&
username === currentUser.value.username
)

const followOrUnfollow = Effect.gen(function*() {
const author = yield* article.author
const updated = author.following
? yield* Profiles.unfollow(author.username)
: yield* Profiles.follow(author.username)
yield* RefSubject.update(ref, (a) => ({ ...a, author: updated }))
})

const favoriteOrUnfavorite = Effect.gen(function*() {
const { favorited, slug } = yield* ref
const updated = yield* favorited ? Articles.unfavorite(slug) : Articles.favorite(slug)
yield* RefSubject.set(ref, updated)
})

const authenticatedActions = Fx.if(isAuthenticated, {
onFalse: Fx.null,
onTrue: html`<button
class="btn btn-sm btn-outline-secondary"
onclick=${followOrUnfollow}
>
<i class="ion-plus-round"></i>
&nbsp;
${
RefSubject.when(author.following, {
onFalse: "Follow",
onTrue: "Unfollow"
})
}
${author.username}
</button>
&nbsp;&nbsp;
<button
class="btn btn-sm btn-outline-primary"
onclick=${favoriteOrUnfavorite}
>
<i class="ion-heart"></i>
&nbsp;
${
RefSubject.when(article.favorited, {
onFalse: "Favorite",
onTrue: "Unfavorite"
})
}
Post
<span class="counter">(${article.favoritesCount})</span>
</button>`
})

const editArticleHref = RefSubject.map(article.slug, (slug) => Routes.editArticle.route.interpolate({ slug }))

const deleteArticle = Effect.gen(function*() {
const slug = yield* article.slug
yield* Articles.delete({ slug })
yield* Router.navigate(Routes.home)
})

const currentUserActions = Fx.if(currentUserIsAuthor, {
onFalse: Fx.null,
onTrue: html`&nbsp;&nbsp;
${
Link(
{
to: editArticleHref,
className: "btn btn-sm btn-outline-secondary",
relative: false
},
html`<i class="ion-edit"></i> Edit Article`
)
}
&nbsp;&nbsp;
<button class="btn btn-sm btn-outline-danger" onclick=${deleteArticle}>
<i class="ion-trash-a"></i> Delete Article
</button>`
})

const meta = html`<div class="article-meta">
${
Link(
{ to: authorProfileHref, relative: false },
html`<img src="${authorImage}" />`
)
}
<div class="info">
${
Link(
{ to: authorProfileHref, className: "author", relative: false },
author.username
)
}
<span class="date">${createdDate}</span>
</div>
${authenticatedActions} ${currentUserActions}
</div>`
const meta = ArticleMeta(ref)
const postComment = PostComment(article.slug, (comment) => RefArray.append(comments, comment))

return html`<div class="article-page">
<div class="banner">
Expand All @@ -142,13 +48,7 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
<div class="col-md-12">
<p>${article.body}</p>
<ul class="tag-list">
${
many(
article.tagList,
(t) => t,
(tag) => html`<li class="tag-default tag-pill tag-outline">${tag}</li>`
)
}
${many(article.tagList, (t) => t, renderTag)}
</ul>
</div>
</div>
Expand All @@ -159,24 +59,127 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
${
Fx.if(isAuthenticated, {
onFalse: html`<p show-authed="false" style="display: inherit;">
${Link({ to: "/login", relative: false }, "Sign in")}
or
${Link({ to: "/register", relative: false }, "sign up")}
to add comments on this article.
</p>`,
onTrue: PostComment(article.slug, (comment) => RefArray.append(comments, comment))
})
}
${Fx.if(isAuthenticated, { onFalse: signInOrSignUp, onTrue: postComment })}
${many(comments, (c) => c.id, CommentCard)}
</div>
</div>
</div>
</div>`
})

function ArticleMeta<E, R>(ref: RefSubject.RefSubject<Article, E, R>) {
const article = RefSubject.proxy(ref)
const author = RefSubject.proxy(article.author)
const authorProfileHref = RefSubject.map(author.username, (username) => Routes.profile.interpolate({ username }))
const authorImage = RefSubject.map(author.image, (img) => Option.getOrElse(img, () => FALLBACK_IMAGE))
const createdDate = RefSubject.map(article.createdAt, formatMonthDayYear)
const currentUserIsAuthor = RefSubject.map(
RefSubject.struct({
username: author.username,
currentUser: CurrentUser
}),
({ currentUser, username }) =>
AsyncData.isSuccess(currentUser) &&
username === currentUser.value.username
)

const followOrUnfollow = Effect.gen(function*() {
const author = yield* article.author
const updated = author.following
? yield* Profiles.unfollow(author.username)
: yield* Profiles.follow(author.username)
yield* RefSubject.update(ref, (a) => ({ ...a, author: updated }))
})

const favoriteOrUnfavorite = Effect.gen(function*() {
const { favorited, slug } = yield* ref
const updated = yield* favorited
? Articles.unfavorite(slug)
: Articles.favorite(slug)
yield* RefSubject.set(ref, updated)
})

const authenticatedActions = Fx.if(isAuthenticated, {
onFalse: Fx.null,
onTrue: html`<button
class="btn btn-sm btn-outline-secondary"
onclick=${followOrUnfollow}
>
<i class="ion-plus-round"></i>
&nbsp;
${
RefSubject.when(author.following, {
onFalse: "Follow",
onTrue: "Unfollow"
})
}
${author.username}
</button>
&nbsp;&nbsp;
<button
class="btn btn-sm btn-outline-primary"
onclick=${favoriteOrUnfavorite}
>
<i class="ion-heart"></i>
&nbsp;
${
RefSubject.when(article.favorited, {
onFalse: "Favorite",
onTrue: "Unfavorite"
})
}
Post
<span class="counter">(${article.favoritesCount})</span>
</button>`
})

const editArticleHref = RefSubject.map(article.slug, (slug) => Routes.editArticle.route.interpolate({ slug }))

const deleteArticle = Effect.gen(function*() {
const slug = yield* article.slug
yield* Articles.delete({ slug })
yield* Router.navigate(Routes.home)
})

const currentUserActions = Fx.if(currentUserIsAuthor, {
onFalse: Fx.null,
onTrue: html`&nbsp;&nbsp;
${
Link(
{
to: editArticleHref,
className: "btn btn-sm btn-outline-secondary",
relative: false
},
html`<i class="ion-edit"></i> Edit Article`
)
}
&nbsp;&nbsp;
<button class="btn btn-sm btn-outline-danger" onclick=${deleteArticle}>
<i class="ion-trash-a"></i> Delete Article
</button>`
})

return html`<div class="article-meta">
${
Link(
{ to: authorProfileHref, relative: false },
html`<img src="${authorImage}" />`
)
}
<div class="info">
${
Link(
{ to: authorProfileHref, className: "author", relative: false },
author.username
)
}
<span class="date">${createdDate}</span>
</div>
${authenticatedActions} ${currentUserActions}
</div>`
}

function PostComment<E, R, E2, R2>(
slug: RefSubject.Computed<ArticleSlug, E, R>,
onNewComment: (comment: Comment) => Effect.Effect<void, E2, R2>
Expand All @@ -194,7 +197,9 @@ function PostComment<E, R, E2, R2>(
const body = yield* commentBody
if (body.trim() === "") return

const comment = yield* Comments.create(yield* slug, { body: CommentBody.make(body) })
const comment = yield* Comments.create(yield* slug, {
body: CommentBody.make(body)
})
yield* onNewComment(comment)
yield* RefSubject.set(commentBody, "")
})
Expand Down Expand Up @@ -232,7 +237,10 @@ function CommentCard(ref: RefSubject.RefSubject<Comment>) {
const comment = RefSubject.proxy(ref)
const author = RefSubject.proxy(comment.author)
const authorProfileHref = RefSubject.map(author.username, (username) => Routes.profile.interpolate({ username }))
const authorImage = RefSubject.map(author.image, Option.getOrElse(() => FALLBACK_IMAGE))
const authorImage = RefSubject.map(
author.image,
Option.getOrElse(() => FALLBACK_IMAGE)
)
const datePosted = RefSubject.map(comment.createdAt, formatMonthAndDay)

return html`<div class="card">
Expand Down

0 comments on commit b177aa1

Please sign in to comment.