Skip to content

Commit

Permalink
refactor: consolidate routes for interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed May 19, 2024
1 parent 4b067f8 commit b78d7c1
Show file tree
Hide file tree
Showing 14 changed files with 86 additions and 93 deletions.
25 changes: 25 additions & 0 deletions examples/realworld/src/ui/common/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Route, Router } from "@typed/core"
import { ArticleSlug, Username } from "@typed/realworld/model"
import { isAuthenticatedGuard } from "@typed/realworld/services"

export const home = Route.home.concat(Route.queryParams({
tag: Route.param("tag").optional(),
page: Route.integer("page").optional(),
myFeed: Route.boolean("myFeed").optional()
}))

export const article = Route.literal("article").concat(
Route.paramWithSchema("slug", ArticleSlug)
)

export const editor = Route.literal("editor").pipe(isAuthenticatedGuard)

export const editArticle = Router.concat(editor, Route.paramWithSchema("slug", ArticleSlug))

export const login = Route.literal("login")

export const profile = Route.literal("profile").concat(Route.paramWithSchema("username", Username))

export const register = Route.literal("register")

export const settings = Route.literal("settings").pipe(isAuthenticatedGuard)
5 changes: 3 additions & 2 deletions examples/realworld/src/ui/components/ArticlePreview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Fx, Link, RefSubject } from "@typed/core"
import { type Article, Image } from "@typed/realworld/model"
import * as Routes from "@typed/realworld/ui/common/routes"
import { html, many } from "@typed/template"
import { Effect, Option } from "effect"
import { Articles, isAuthenticated } from "../../services"
Expand All @@ -10,8 +11,8 @@ const FALLBACK_IMAGE = Image.make("https://api.realworld.io/images/demo-avatar.p
export function ArticlePreview(ref: RefSubject.RefSubject<Article>) {
const article = RefSubject.proxy(ref)
const author = RefSubject.proxy(article.author)
const articleHref = RefSubject.map(article.slug, (slug) => `/article/${slug}`)
const userProfileHref = RefSubject.map(author.username, (username) => `/profile/${username}`)
const articleHref = RefSubject.map(article.slug, (slug) => Routes.article.interpolate({ slug }))
const userProfileHref = RefSubject.map(author.username, (username) => Routes.profile.interpolate({ username }))
const userProfileImage = RefSubject.map(author.image, Option.getOrElse(() => FALLBACK_IMAGE))
const createdDate = RefSubject.map(article.createdAt, formatMonthDayYear)

Expand Down
3 changes: 2 additions & 1 deletion examples/realworld/src/ui/components/EditArticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ArticleTitle
} from "@typed/realworld/model"
import type { Unauthorized, Unprocessable } from "@typed/realworld/services/errors"
import * as Routes from "@typed/realworld/ui/common/routes"
import { Effect } from "effect"

export type EditArticleFields = Pick<
Expand Down Expand Up @@ -39,7 +40,7 @@ export function useEditArticle<R, R2>(
Effect.flatMap(onSubmit),
Effect.catchTags({
Unprocessable: (error) => RefSubject.set(errors, error.errors),
Unauthorized: () => navigate("/login"),
Unauthorized: () => navigate(Routes.login.interpolate({})),
ParseError: (issue) => RefSubject.set(errors, [issue.message])
})
)
Expand Down
19 changes: 4 additions & 15 deletions examples/realworld/src/ui/components/Pagination.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { html, Link, many, RefSubject } from "@typed/core"
import { CurrentPath, getCurrentPathFromUrl } from "@typed/navigation"
import { CurrentSearchParams } from "@typed/router"

export function usePagination<E, R>(pageSize: number, count: RefSubject.Computed<number, E, R>) {
export function Pagination<E, R>(pageSize: number, count: RefSubject.Computed<number, E, R>) {
const currentPage = RefSubject.map(CurrentSearchParams, (params) => Number(params.page ?? 1))
const pages = RefSubject.map(count, (count) => Array.from({ length: Math.ceil(count / pageSize) }, (_, i) => i + 1))
const view = html`<ul class="pagination">

return html`<ul class="pagination">
${many(pages, (p) => p, (page) => renderPagination(currentPage, page))}
</ul>`

return {
view,
currentPage
} as const
}

function renderPagination<R>(
Expand All @@ -23,13 +18,7 @@ function renderPagination<R>(
RefSubject.map(([current, p]) => current === p ? "active" : "")
)

const to = RefSubject.map(RefSubject.tuple([CurrentPath, page]), ([path, page]) => {
const url = new URL(path, "http://localhost")
url.searchParams.set("page", page.toString())
return getCurrentPathFromUrl(url)
})

return html`<li class="page-item ${activeClassName}">
${Link({ to, className: "page-link", relative: false }, page)}
${Link({ to: RefSubject.map(page, (p) => `?page=${p}`), className: "page-link" }, page)}
</li>`
}
20 changes: 9 additions & 11 deletions examples/realworld/src/ui/pages/article.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { AsyncData, Fx, Link, RefArray, RefSubject } from "@typed/core"
import type { EventWithTarget } from "@typed/dom/EventTarget"
import { navigate } from "@typed/navigation"
import type { Comment } from "@typed/realworld/model"
import { ArticleSlug, CommentBody, Image } from "@typed/realworld/model"
import type { ArticleSlug, 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"
import * as Route from "@typed/route"
import * as Routes from "@typed/realworld/ui/common/routes"
import type * as Route from "@typed/route"
import { EventHandler, html, many } from "@typed/template"
import { Effect } from "effect"
import * as Option from "effect/Option"

export const route = Route.literal("article").concat(
Route.paramWithSchema("slug", ArticleSlug)
)

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")
Expand All @@ -23,7 +21,7 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
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) => `/profile/${username}`)
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)
Expand Down Expand Up @@ -85,12 +83,12 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
</button>`
})

const editArticleHref = RefSubject.map(article.slug, (slug) => `/editor/${slug}`)
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* navigate("/")
yield* navigate(Routes.home.interpolate({}))
})

const currentUserActions = Fx.if(currentUserIsAuthor, {
Expand Down Expand Up @@ -234,7 +232,7 @@ function PostComment<E, R, E2, R2>(
function CommentCard(ref: RefSubject.RefSubject<Comment>) {
const comment = RefSubject.proxy(ref)
const author = RefSubject.proxy(comment.author)
const authorProfileHref = RefSubject.map(author.username, (x) => `/profile/${x}`)
const authorProfileHref = RefSubject.map(author.username, (username) => Routes.profile.interpolate({ username }))
const authorImage = RefSubject.map(author.image, Option.getOrElse(() => FALLBACK_IMAGE))
const datePosted = RefSubject.map(comment.createdAt, formatMonthAndDay)

Expand Down
9 changes: 3 additions & 6 deletions examples/realworld/src/ui/pages/edit-article.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Fx, RefSubject } from "@typed/core"
import { ArticleSlug } from "@typed/realworld/model"
import { Articles, isAuthenticatedGuard } from "@typed/realworld/services"
import * as Route from "@typed/route"
import { Articles } from "@typed/realworld/services"
import * as Routes from "@typed/realworld/ui/common/routes"
import type { RouteGuard } from "@typed/router"
import { Effect } from "effect"
import { EditArticle } from "../components/EditArticle"

export const route = Route.literal("editor")
.concat(Route.paramWithSchema("slug", ArticleSlug))
.pipe(isAuthenticatedGuard)
export const route = Routes.editArticle

export type Params = RouteGuard.RouteGuard.Success<typeof route>

Expand Down
9 changes: 5 additions & 4 deletions examples/realworld/src/ui/pages/editor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Fx, Route } from "@typed/core"
import { Fx } from "@typed/core"
import { RefSubject } from "@typed/fx"
import { navigate } from "@typed/navigation"
import { ArticleBody, ArticleDescription, ArticleTitle } from "@typed/realworld/model"
import { Articles, isAuthenticatedGuard } from "@typed/realworld/services"
import { Articles } from "@typed/realworld/services"
import * as Routes from "@typed/realworld/ui/common/routes"
import { Effect } from "effect"
import { EditArticle, type EditArticleFields } from "../components/EditArticle"

export const route = Route.literal("editor").pipe(isAuthenticatedGuard)
export const route = Routes.editor

export const main = Fx.gen(function*() {
const initial = yield* RefSubject.of<EditArticleFields>({
Expand All @@ -21,7 +22,7 @@ export const main = Fx.gen(function*() {
(input) =>
Effect.gen(function*(_) {
const article = yield* _(Articles.create(input))
yield* navigate(`/article/${article.slug}`)
yield* navigate(Routes.article.interpolate({ slug: article.slug }))
})
)
})
20 changes: 6 additions & 14 deletions examples/realworld/src/ui/pages/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,17 @@ import { ArticleTag } from "@typed/realworld/model"
import { Articles, isAuthenticated, Tags } from "@typed/realworld/services"
import type { GetArticlesInput } from "@typed/realworld/services/GetArticles"
import { defaultGetArticlesInput } from "@typed/realworld/services/GetArticles"
import * as Routes from "@typed/realworld/ui/common/routes"
import { ArticlePreview } from "@typed/realworld/ui/components/ArticlePreview"
import { NavLink } from "@typed/realworld/ui/components/NavLink"
import { usePagination } from "@typed/realworld/ui/components/Pagination"
import * as Route from "@typed/route"
import { Pagination } from "@typed/realworld/ui/components/Pagination"
import type * as Route from "@typed/route"
import { html, many } from "@typed/template"
import { Option } from "effect"

const pageSize = 20

export const route = Route.home.pipe(
Route.concat(
Route.queryParams({
tag: Route.param("tag").optional(),
page: Route.integer("page").optional(),
myFeed: Route.boolean("myFeed").optional()
})
)
)
export const route = Routes.home

export const main = (
params: RefSubject.RefSubject<Route.Route.Type<typeof route>>
Expand All @@ -49,7 +42,6 @@ export const main = (
)
const { articles, articlesCount } = RefSubject.proxy(feed)
const tagsList = yield* _(RefArray.make(Tags.get()))
const pagination = usePagination(pageSize, articlesCount)

return html`<div class="home-page">
<div class="banner">
Expand Down Expand Up @@ -82,7 +74,7 @@ export const main = (
${many(articles, (a) => a.id, ArticlePreview)}
${pagination.view}
${Pagination(pageSize, articlesCount)}
</div>
<div class="col-md-3">
Expand All @@ -97,7 +89,7 @@ export const main = (
(t) =>
Link(
{
to: RefSubject.map(t, (t) => `/?tag=${t}`),
to: RefSubject.map(t, (tag) => Routes.home.interpolate({ tag })),
className: "tag-pill tag-default",
relative: false
},
Expand Down
26 changes: 9 additions & 17 deletions examples/realworld/src/ui/pages/login.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ArrayFormatter } from "@effect/schema"
import { AsyncData, EventHandler, Fx, html, Navigation, RefAsyncData, RefSubject, Route } from "@typed/core"
import { AsyncData, EventHandler, Fx, html, Navigation, RefAsyncData, RefSubject } from "@typed/core"
import type { EventWithTarget } from "@typed/dom/EventTarget"
import { isDom } from "@typed/environment"
import { RedirectError } from "@typed/navigation"
import { parseFormData } from "@typed/realworld/lib/Schema"
import { CurrentUser, isAuthenticated, Users } from "@typed/realworld/services"
import { Unprocessable } from "@typed/realworld/services/errors"
import { LoginInput } from "@typed/realworld/services/Login"
import * as Routes from "@typed/realworld/ui/common/routes"
import { CurrentUserErrors } from "@typed/realworld/ui/services/CurrentUser"
import { Effect } from "effect"

export const route = Route.literal("/login")
export const route = Routes.login

type SubmitEvent = EventWithTarget<HTMLFormElement, Event>

Expand All @@ -19,13 +20,9 @@ export const main = Fx.gen(function*(_) {
Effect.zipRight(loginUser(ev), RefSubject.set(hasSubmitted, true))
)

if (yield* _(isDom)) {
yield* _(
isAuthenticated,
Effect.if({
onFalse: () => Effect.void,
onTrue: () => Navigation.navigate("/", { history: "replace" })
})
if (yield* _(isAuthenticated)) {
return yield* _(
new RedirectError({ path: Routes.home.interpolate({}) })
)
}

Expand All @@ -38,12 +35,7 @@ export const main = Fx.gen(function*(_) {
<a href="/register">Need an account?</a>
</p>
${
Fx.if(hasSubmitted, {
onFalse: Fx.null,
onTrue: CurrentUserErrors
})
}
${Fx.if(hasSubmitted, { onFalse: Fx.null, onTrue: CurrentUserErrors })}
<form onsubmit=${onSubmit}>
<fieldset class="form-group">
Expand Down Expand Up @@ -86,7 +78,7 @@ function loginUser(ev: SubmitEvent) {
const updated = yield* _(RefAsyncData.runAsyncData(CurrentUser, Users.login(input)))

if (AsyncData.isSuccess(updated)) {
yield* _(Navigation.navigate("/", { history: "replace" }))
yield* _(Navigation.navigate(Routes.home.interpolate({}), { history: "replace" }))
}
}),
"ParseError",
Expand Down
11 changes: 5 additions & 6 deletions examples/realworld/src/ui/pages/profile.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Schema } from "@effect/schema"
import { Fx, html, Link, many, RefSubject, Router } from "@typed/core"
import { Username } from "@typed/realworld/model"
import { Articles, Profiles } from "@typed/realworld/services"
import { defaultGetArticlesInput } from "@typed/realworld/services/GetArticles"
import * as Routes from "@typed/realworld/ui/common/routes"
import { ArticlePreview } from "@typed/realworld/ui/components/ArticlePreview"
import * as Route from "@typed/route"
import { CurrentSearchParams } from "@typed/router"
import { Effect, Option } from "effect"
import { NavLink } from "../components/NavLink"
import { usePagination } from "../components/Pagination"
import { Pagination } from "../components/Pagination"

export const route = Route.literal("profile").concat(Route.paramWithSchema("username", Username))
export const route = Routes.profile

const favoritesRoute = Route.literal("favorites")
const pageSize = 5
Expand All @@ -37,7 +37,6 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
})
)
const { articles, articlesCount } = RefSubject.proxy(articlesAndCount)
const pagination = usePagination(pageSize, articlesCount)
const followOrUnfollow = Effect.gen(function*() {
const current = yield* ref
const updated = current.following
Expand Down Expand Up @@ -65,7 +64,7 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
${
Link(
{
to: "/settings",
to: Routes.settings.route.interpolate({}),
relative: false
},
html`<button
Expand Down Expand Up @@ -106,7 +105,7 @@ export const main = (params: RefSubject.RefSubject<Params>) =>
${many(articles, (a) => a.id, ArticlePreview)}
${pagination.view}
${Pagination(pageSize, articlesCount)}
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit b78d7c1

Please sign in to comment.