Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- module App
- open Elmish
- open Elmish.React
- open Feliz
- open Fable.SimpleHttp
- open Fable.DateFunctions
- open Thoth.Json
- open System
- module Async =
- let map f op = async {
- let! x = op
- let value = f x
- return value
- }
- module Cmd =
- let fromAsync (operation:Async<'msg>) : Cmd<'msg> =
- let delayedCmd (dispatch : 'msg -> unit) : unit =
- let delayedDispatch = async {
- let! msg = operation
- dispatch msg
- }
- Async.StartImmediate delayedDispatch
- Cmd.ofSub delayedCmd
- [<AutoOpen>]
- module AsyncOperations =
- type Deferred<'t> =
- | HasNotStartedYet
- | InProgress
- | Resolved of 't
- type AsyncOperationStatus<'t> =
- | Started
- | Finished of 't
- type HackernewsItem = {
- Time : int
- }
- type DeferredResult<'t> = Deferred<Result<'t, string>>
- type DeferredStoryItem = DeferredResult<HackernewsItem>
- type State = {
- StoryItems: DeferredResult<Map<int,DeferredStoryItem>>
- }
- type Msg =
- | LoadStoryItems of AsyncOperationStatus<Result<int list, string>>
- | LoadedStoryItem of int * Result<HackernewsItem, string>
- let init() =
- let initialState = {
- StoryItems = HasNotStartedYet
- }
- let initialCmd = Cmd.ofMsg (LoadStoryItems Started)
- initialState, initialCmd
- let itemDecoder : Decoder<HackernewsItem> =
- Decode.object (fun fields -> {
- Time = fields.Required.At ["time"] Decode.int
- })
- let storiesEndpoint = "https://hacker-news.firebaseio.com/v0/topstories.json"
- let loadStoryItem (itemId:int) = async {
- let rnd = System.Random()
- let endpoint = sprintf "https://hacker-news.firebaseio.com/v0/item/%d.json" itemId
- do! Async.Sleep (rnd.Next(1000, 3000))
- let! (status, responseText) = Http.get endpoint
- match status with
- | 200 ->
- match Decode.fromString itemDecoder responseText with
- | Ok storyItem ->
- return LoadedStoryItem(itemId, Ok storyItem)
- | Error errorMsg ->
- return LoadedStoryItem(itemId, Error errorMsg)
- | _ ->
- return LoadedStoryItem(itemId, Error("Http error while loading " + string itemId))
- }
- let loadStoryItems = async {
- let endpoint = storiesEndpoint
- let! (status, responseText) = Http.get endpoint
- match status with
- | 200 ->
- let storyIds = Decode.fromString (Decode.list Decode.int) responseText
- match storyIds with
- | Ok storyIds ->
- let storyItems =
- storyIds
- |> List.truncate 10
- return LoadStoryItems(Finished (Ok storyItems))
- | Error errorMsg ->
- return LoadStoryItems (Finished (Error errorMsg))
- | _ ->
- return LoadStoryItems (Finished (Error("Could not load the IDs of the story items.")))
- }
- let startLoading (state:State) =
- { state with StoryItems = InProgress }
- let update msg state =
- match msg with
- | LoadStoryItems Started ->
- let nextState = { state with StoryItems = InProgress }
- let nextCmd = Cmd.fromAsync loadStoryItems
- nextState, nextCmd
- | LoadStoryItems (Finished (Ok storyIds)) ->
- let storiesMap = Map.ofList [ for id in storyIds -> id, Deferred.InProgress ]
- let nextState = { state with StoryItems = Resolved(Ok storiesMap) }
- nextState, Cmd.batch [for id in storyIds -> Cmd.fromAsync (loadStoryItem id)]
- | LoadStoryItems (Finished (Error error)) ->
- let nextState = { state with StoryItems = Resolved (Error error) }
- nextState, Cmd.none
- | LoadedStoryItem (itemId, Ok item) ->
- match state.StoryItems with
- | Resolved (Ok storiesMap) ->
- let modifiedStoriesMap =
- storiesMap
- |> Map.remove itemId
- |> Map.add itemId (Resolved (Ok item))
- let nextState = { state with StoryItems = Resolved(Ok modifiedStoriesMap) }
- nextState, Cmd.none
- | _ ->
- state, Cmd.none
- | LoadedStoryItem (itemId, Error error) ->
- match state.StoryItems with
- | Resolved (Ok storiesMap) ->
- let modifiedStoriesMap =
- storiesMap
- |> Map.remove itemId
- |> Map.add itemId (Resolved (Error error))
- let nextState = { state with StoryItems = Resolved (Ok modifiedStoriesMap) }
- nextState, Cmd.none
- | _ ->
- state, Cmd.none
- let renderError (errorMsg : string) =
- Html.h1 [
- prop.style [ style.color.red ]
- prop.text errorMsg
- ]
- let div (classes: string list) (children: ReactElement list) =
- Html.div [
- prop.className classes
- prop.children children
- ]
- let (|Years|Months|Weeks|Days|Hours|Minutes|Seconds|) (time:int) =
- let time = DateTime.Parse time
- match (DateTime.Now.DifferenceInYears time) with
- | value when value > 0 ->
- Years value
- | _ ->
- match (DateTime.Now.DifferenceInMonths time) with
- | value when value > 0 ->
- Months value
- | _ ->
- match (DateTime.Now.DifferenceInWeeks time) with
- | value when value > 0 ->
- Weeks value
- | _ ->
- match (DateTime.Now.DifferenceInDays time) with
- | value when value > 0 ->
- Days value
- | _ ->
- match (DateTime.Now.DifferenceInHours time) with
- | value when value > 0 ->
- Hours value
- | _ ->
- match (DateTime.Now.DifferenceInMinutes time) with
- | value when value > 0 ->
- Minutes value
- | value ->
- Seconds value
- let renderItemTime item =
- match item.Time with
- | Years y -> sprintf "%d years ago" y
- | Months m -> sprintf "%d months ago" m
- | Weeks w -> sprintf "%d weeks ago" w
- | Days d -> sprintf "%d days ago" d
- | Hours h -> sprintf "%d hours ago" h
- | Minutes m -> sprintf "%d minutes ago" m
- | Seconds s -> sprintf "%d seconds ago" s
- let renderItemContent item =
- Html.div [
- prop.children [
- div [ "columns"; "is-mobile" ] [
- div [ "column"; "is-narrow" ] [
- Html.div [
- prop.className [ "icon" ]
- prop.style [ style.marginLeft 20 ]
- prop.children [
- Html.i [prop.className "fa fa-poll fa-2x"]
- ]
- ]
- ]
- div ["column"; "is-narrow"] [
- Html.div [
- prop.children [
- Html.span [
- prop.style [style.marginLeft 10; style.marginRight 10 ]
- prop.text (renderItemTime item)
- ]
- ]
- ]
- ]
- ]
- ]
- ]
- let spinner =
- Html.div [
- prop.style [ style.textAlign.center; style.marginTop 20 ]
- prop.children [
- Html.i [
- prop.className "fa fa-cog fa-spin fa-2x"
- ]
- ]
- ]
- let renderStoryItem (itemId:int) storyItem =
- let renderedItem =
- match storyItem with
- | HasNotStartedYet -> Html.none
- | InProgress -> spinner
- | Resolved (Error errorMsg) -> renderError errorMsg
- | Resolved (Ok item) -> renderItemContent item
- Html.div [
- prop.key itemId
- prop.className "box"
- prop.style [style.marginTop 15; style.marginBottom 15]
- prop.children [ renderedItem ]
- ]
- let renderStories items =
- match items with
- | HasNotStartedYet -> Html.none
- | InProgress -> spinner
- | Resolved (Error errorMsg) -> renderError errorMsg
- | Resolved (Ok items) ->
- items
- |> Map.toList
- |> List.sortByDescending (fun (_, item) -> item)
- |> List.map (fun (id, storyItem) -> renderStoryItem id storyItem)
- |> Html.div
- let render (state: State) (dispatch: Msg -> unit) =
- Html.div [
- prop.style [ style.padding 20 ]
- prop.children [
- Html.h1 [
- prop.className "title"
- prop.text "Elmish Hackernews"
- ]
- renderStories state.StoryItems
- ]
- ]
- Program.mkProgram init update render
- |> Program.withReactSynchronous "elmish-app"
- |> Program.run
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement