From c1f0cfa69ec54f39e5249720e7891944c3fe1631 Mon Sep 17 00:00:00 2001 From: Muhammad Zaki <muhammad.zaki93@ui.ac.id> Date: Wed, 30 Nov 2022 21:11:26 +0700 Subject: [PATCH] feat(client): user page --- client/src/components/User.res | 7 +++ client/src/lib/Api.res | 93 ++++++++++++++++++++++++++++---- client/src/pages/StoriesPage.res | 90 ++++++++++++++++--------------- client/src/pages/StoryPage.res | 76 +++++++++++++------------- client/src/pages/UserPage.res | 55 ++++++++++++++++++- 5 files changed, 230 insertions(+), 91 deletions(-) create mode 100644 client/src/components/User.res diff --git a/client/src/components/User.res b/client/src/components/User.res new file mode 100644 index 0000000..ed201a5 --- /dev/null +++ b/client/src/components/User.res @@ -0,0 +1,7 @@ +type t = { + id: string, + created: int, + karma: int, + submitted: array<int>, + about?: string, +} diff --git a/client/src/lib/Api.res b/client/src/lib/Api.res index 73ca7ff..bcb22aa 100644 --- a/client/src/lib/Api.res +++ b/client/src/lib/Api.res @@ -15,39 +15,112 @@ function fetchAPI(path) { const url = path.startsWith("user") ? user(path) : story(path); const { abort, ready } = abortableFetch(url); - return { abort, ready: ready.then((r) => r.json()) }; + return { abort, ready: ready.then((r) => { + if (r.ok) { + return r.json() + } else { + throw Error(r.statusText) + } + }) }; }`) -external fetch: string => {"abort": unit => unit, "ready": Js.Promise.t<'data>} = "fetchAPI" +type abortable_fetch<'data> = { + abort: unit => unit, + ready: Js.Promise.t<'data>, +} + +external fetch: string => abortable_fetch<'data> = "fetchAPI" let fetch = fetch let useStoryData = (id: option<string>) => { - let (story, setStory) = React.useState((_): option<Story.t> => None) + let (story, setStory) = React.useState((_): result<option<Story.t>, string> => Ok(None)) React.useEffect1(() => { let abortableFetch = fetch(j`item/$id`) if id->Belt.Option.isSome { - abortableFetch["ready"] - ->Promise.then(fetchedStory => setStory(_ => Some(fetchedStory))->Promise.resolve) + abortableFetch.ready + ->Promise.then(fetchedStory => setStory(_ => fetchedStory->Some->Ok)->Promise.resolve) + ->Promise.catch(e => { + let msg = switch e { + | Promise.JsError(err) => + switch err->Js.Exn.name { + | Some("AbortError") => None + | _ => err->Js.Exn.message->Belt.Option.getWithDefault("")->Some + } + | _ => "Unexpected error occurred"->Some + } + + switch msg { + | Some(msg) => setStory(_ => msg->Error) + | None => () + }->Promise.resolve + }) ->ignore } - Some(abortableFetch["abort"]) + abortableFetch.abort->Some }, [id]) story } let useStoriesData = (type_: string, page: int) => { - let (stories, setStories) = React.useState((_): array<Story.t> => []) + let (stories, setStories) = React.useState((_): result<array<Story.t>, string> => []->Ok) React.useEffect2(() => { let abortableFetch = fetch(j`$type_?page=$page`) - abortableFetch["ready"] - ->Promise.then(fetchedStories => setStories(_ => fetchedStories)->Promise.resolve) + abortableFetch.ready + ->Promise.then(fetchedStories => setStories(_ => fetchedStories->Ok)->Promise.resolve) + ->Promise.catch(e => { + let msg = switch e { + | Promise.JsError(err) => + switch err->Js.Exn.name { + | Some("AbortError") => None + | _ => err->Js.Exn.message->Belt.Option.getWithDefault("")->Some + } + | _ => "Unexpected error occurred"->Some + } + + switch msg { + | Some(msg) => setStories(_ => msg->Error) + | None => () + }->Promise.resolve + }) ->ignore - Some(abortableFetch["abort"]) + + abortableFetch.abort->Some }, (type_, page)) stories } + +let useUserData = (id: option<string>) => { + let (user, setUser) = React.useState((_): result<option<User.t>, string> => Ok(None)) + + React.useEffect1(() => { + let abortableFetch = fetch(j`user/$id`) + if id->Belt.Option.isSome { + abortableFetch.ready + ->Promise.then(fetchedUser => setUser(_ => fetchedUser->Ok)->Promise.resolve) + ->Promise.catch(e => { + let msg = switch e { + | Promise.JsError(err) => + switch err->Js.Exn.name { + | Some("AbortError") => None + | _ => err->Js.Exn.message->Belt.Option.getWithDefault("")->Some + } + | _ => "Unexpected error occurred"->Some + } + + switch msg { + | Some(msg) => setUser(_ => msg->Error) + | None => () + }->Promise.resolve + }) + ->ignore + } + abortableFetch.abort->Some + }, [id]) + + user +} diff --git a/client/src/pages/StoriesPage.res b/client/src/pages/StoriesPage.res index 943a07e..5a08d55 100644 --- a/client/src/pages/StoriesPage.res +++ b/client/src/pages/StoriesPage.res @@ -17,48 +17,52 @@ let useData = () => { let make = () => { let (stories, page) = useData() - <div className="news-view"> - <div className="news-list-nav"> - <Show - when_={page > 1} - fallback={<span className="page-link disabled" ariaHidden=true> - {"< prev"->React.string} - </span>}> - <Link - className="page-link" - href={ - let prevPage = page - 1 - j`/?page=$prevPage` - } - ariaLabel="Previous Page"> - {"< prev"->React.string} - </Link> - </Show> - <span> {j`page $page`->React.string} </span> - <Show - when_={page < 10} - fallback={<span className="page-link disabled" ariaHidden={true}> - {"more >"->React.string} - </span>}> - <Link - className="page-link" - href={ - let nextPage = page + 1 - j`/?page=$nextPage` - } - ariaLabel="Next Page"> - {"more >"->React.string} - </Link> - </Show> + switch stories { + | Ok(stories) => + <div className="news-view"> + <div className="news-list-nav"> + <Show + when_={page > 1} + fallback={<span className="page-link disabled" ariaHidden=true> + {"< prev"->React.string} + </span>}> + <Link + className="page-link" + href={ + let prevPage = page - 1 + j`/?page=$prevPage` + } + ariaLabel="Previous Page"> + {"< prev"->React.string} + </Link> + </Show> + <span> {j`page $page`->React.string} </span> + <Show + when_={page < 10} + fallback={<span className="page-link disabled" ariaHidden={true}> + {"more >"->React.string} + </span>}> + <Link + className="page-link" + href={ + let nextPage = page + 1 + j`/?page=$nextPage` + } + ariaLabel="Next Page"> + {"more >"->React.string} + </Link> + </Show> + </div> + <main className="news-list"> + <Show when_={stories->Js.Array2.length > 0}> + <ul> + {stories + ->Belt.Array.map(story => <Story key={Js.Int.toString(story.id)} story />) + ->React.array} + </ul> + </Show> + </main> </div> - <main className="news-list"> - <Show when_={stories->Js.Array2.length > 0}> - <ul> - {stories - ->Belt.Array.map(story => <Story key={Js.Int.toString(story.id)} story />) - ->React.array} - </ul> - </Show> - </main> - </div> + | Error(msg) => <span> {j`Oops, an error has occured: $msg`->React.string} </span> + } } diff --git a/client/src/pages/StoryPage.res b/client/src/pages/StoryPage.res index 3b573c4..89a8223 100644 --- a/client/src/pages/StoryPage.res +++ b/client/src/pages/StoryPage.res @@ -13,42 +13,46 @@ let make = () => { let story = useData() switch story { - | Some(story) => - <div className="item-view"> - <div className="item-view-header"> - <a href={story.url->Belt.Option.getWithDefault("")} target="_blank"> - <h1> {story.title->React.string} </h1> - </a> - {switch story.domain { - | Some(domain) => <span className="host"> {domain->React.string} </span> - | None => React.null - }} - <p className="meta"> - {`${story.points->Belt.Int.toString} points | by `->React.string} - <Link href={`/users/${story.user}`}> {story.user->React.string} </Link> - {" "->React.string} - {story.time_ago->React.string} - </p> + | Ok(story) => + switch story { + | Some(story) => + <div className="item-view"> + <div className="item-view-header"> + <a href={story.url->Belt.Option.getWithDefault("")} target="_blank"> + <h1> {story.title->React.string} </h1> + </a> + {switch story.domain { + | Some(domain) => <span className="host"> {domain->React.string} </span> + | None => React.null + }} + <p className="meta"> + {`${story.points->Belt.Int.toString} points | by `->React.string} + <Link href={`/users/${story.user}`}> {story.user->React.string} </Link> + {" "->React.string} + {story.time_ago->React.string} + </p> + </div> + <div className="item-view-comments"> + <p className="item-view-comments-header"> + {( + story.comments_count > 0 + ? `${story.comments_count->Belt.Int.toString} comments` + : "No comments yet." + )->React.string} + </p> + {switch story.comments { + | Some(comments) => + <ul className="comment-children"> + {comments + ->Belt.Array.map(comment => <Comment key={comment.id->Belt.Int.toString} comment />) + ->React.array} + </ul> + | None => React.null + }} + </div> </div> - <div className="item-view-comments"> - <p className="item-view-comments-header"> - {( - story.comments_count > 0 - ? `${story.comments_count->Belt.Int.toString} comments` - : "No comments yet." - )->React.string} - </p> - {switch story.comments { - | Some(comments) => - <ul className="comment-children"> - {comments - ->Belt.Array.map(comment => <Comment key={comment.id->Belt.Int.toString} comment />) - ->React.array} - </ul> - | None => React.null - }} - </div> - </div> - | None => React.null + | None => React.null + } + | Error(msg) => <span> {j`Oops, an error has occured: $msg`->React.string} </span> } } diff --git a/client/src/pages/UserPage.res b/client/src/pages/UserPage.res index c7a8331..7810d0b 100644 --- a/client/src/pages/UserPage.res +++ b/client/src/pages/UserPage.res @@ -1,4 +1,55 @@ +let useData = () => { + let url = RescriptReactRouter.useUrl() + let id = switch url.path { + | list{"users", id} => Some(id) + | _ => None + } + + Api.useUserData(id) +} + @react.component let make = () => { - <div> {"User page"->React.string} </div> -} \ No newline at end of file + let user = useData() + + <div className="user-view"> + {switch user { + | Ok(user) => + switch user { + | Some(user) => + <> + <h1> {`User : ${user.id}`->React.string} </h1> + <ul className="meta"> + <li> + <span className="label"> {"Created:"->React.string} </span> + {user.created->Belt.Int.toString->React.string} + </li> + <li> + <span className="label"> {"Karma:"->React.string} </span> + {user.karma->Belt.Int.toString->React.string} + </li> + {switch user.about { + | Some(about) => + <> + <li dangerouslySetInnerHTML={{"__html": about}} className="about" /> + {" "->React.string} + </> + | None => React.null + }} + </ul> + <p className="links"> + <a href={`https://news.ycombinator.com/submitted?id=${user.id}`}> + {"submissions"->React.string} + </a> + {" | "->React.string} + <a href={`https://news.ycombinator.com/threads?id=${user.id}`}> + {"comments"->React.string} + </a> + </p> + </> + | None => React.null + } + | Error(msg) => <span> {j`Oops, an error has occured: $msg`->React.string} </span> + }} + </div> +} -- GitLab