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