Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9e3cb8119 | ||
|
|
354a707119 | ||
|
|
3ec84cb051 | ||
|
|
f744cad4f9 | ||
|
|
46870e98bf | ||
|
|
176b28ae6b | ||
|
|
75a0c00828 | ||
|
|
90db6e81b0 | ||
|
|
9edc567a13 | ||
|
|
34d3b7e6ec | ||
|
|
acea5cefa0 | ||
|
|
34b7e6e52e | ||
|
|
a28ef48931 | ||
|
|
633a1efbd9 | ||
|
|
412ddaa55f | ||
|
|
3e6c780a93 | ||
|
|
a329bd2f40 | ||
|
|
aed9d1aa5c | ||
|
|
da5af433f6 |
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -11,17 +11,17 @@ import Username exposing (Username)
|
||||
-- TYPES
|
||||
|
||||
|
||||
type alias Cred =
|
||||
-- 👉 TODO make Cred an opaque type, then fix the resulting compiler errors.
|
||||
-- Afterwards, it should no longer be possible for any other module to access
|
||||
-- this `token` vale directly!
|
||||
--
|
||||
-- 💡 HINT: Other modules still depend on being able to access the
|
||||
-- `username` value. Expand this module's API to expose a new way for them
|
||||
-- to access the `username` without also giving them access to `token`.
|
||||
{ username : Username
|
||||
, token : String
|
||||
}
|
||||
type Cred
|
||||
= Cred Username String
|
||||
|
||||
|
||||
|
||||
-- INFO
|
||||
|
||||
|
||||
username : Cred -> Username
|
||||
username (Cred uname _) =
|
||||
uname
|
||||
|
||||
|
||||
|
||||
@@ -40,14 +40,14 @@ decoder =
|
||||
|
||||
|
||||
encodeToken : Cred -> Value
|
||||
encodeToken cred =
|
||||
Encode.string cred.token
|
||||
encodeToken (Cred _ token) =
|
||||
Encode.string token
|
||||
|
||||
|
||||
addHeader : Cred -> RequestBuilder a -> RequestBuilder a
|
||||
addHeader cred builder =
|
||||
addHeader (Cred _ token) builder =
|
||||
builder
|
||||
|> withHeader "authorization" ("Token " ++ cred.token)
|
||||
|> withHeader "authorization" ("Token " ++ token)
|
||||
|
||||
|
||||
addHeaderIfAvailable : Maybe Cred -> RequestBuilder a -> RequestBuilder a
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -159,8 +159,8 @@ slug (Article internals _) =
|
||||
|
||||
|
||||
body : Article Full -> Body
|
||||
body _ =
|
||||
"👉 TODO make this return the article's body"
|
||||
body (Article _ (Full bod)) =
|
||||
bod
|
||||
|
||||
|
||||
|
||||
@@ -181,8 +181,8 @@ mapAuthor transform (Article info extras) =
|
||||
|
||||
|
||||
fromPreview : Body -> Article Preview -> Article Full
|
||||
fromPreview _ _ =
|
||||
"👉 TODO convert from an Article Preview to an Article Full"
|
||||
fromPreview bod (Article info _) =
|
||||
Article info (Full bod)
|
||||
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ fullDecoder : Maybe Cred -> Decoder (Article Full)
|
||||
fullDecoder maybeCred =
|
||||
Decode.succeed Article
|
||||
|> custom (internalsDecoder maybeCred)
|
||||
|> required "body" "👉 TODO use `Body.decoder` (which is a `Decoder Body`) to decode the body into this Article Full"
|
||||
|> required "body" (Decode.map Full Body.decoder)
|
||||
|
||||
|
||||
internalsDecoder : Maybe Cred -> Decoder Internals
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -71,21 +71,16 @@ init session slug =
|
||||
, article = Loading
|
||||
}
|
||||
, Cmd.batch
|
||||
{- 👉 TODO: Oops! These are all `Task` values, not `Cmd` values!
|
||||
|
||||
Use `|> Task.attempt` and `|> Task.perform` to make this compile.
|
||||
|
||||
Relevant docs:
|
||||
|
||||
https://alpha.elm-lang.org/packages/elm/core/latest/Task#attempt
|
||||
https://alpha.elm-lang.org/packages/elm/core/latest/Task#perform
|
||||
-}
|
||||
[ Article.fetch maybeCred slug
|
||||
|> Http.toTask
|
||||
|> Task.attempt CompletedLoadArticle
|
||||
, Comment.list maybeCred slug
|
||||
|> Http.toTask
|
||||
|> Task.attempt CompletedLoadComments
|
||||
, Time.here
|
||||
|> Task.perform GotTimeZone
|
||||
, Loading.slowThreshold
|
||||
|> Task.perform (\_ -> PassedSlowLoadThreshold)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -103,7 +103,7 @@ view model =
|
||||
Loaded feed ->
|
||||
[ div [ class "feed-toggle" ] <|
|
||||
List.concat
|
||||
[ [ viewTabs model ]
|
||||
[ [ viewTabs model.session model.feedTab ]
|
||||
, Feed.viewArticles model.timeZone feed
|
||||
|> List.map (Html.map GotFeedMsg)
|
||||
, [ Feed.viewPagination ClickedFeedPage feed ]
|
||||
@@ -155,21 +155,16 @@ viewBanner =
|
||||
-- TABS
|
||||
|
||||
|
||||
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
||||
|
||||
💡 HINT: It may end up with multiple arguments!
|
||||
|
||||
-}
|
||||
viewTabs : Model -> Html Msg
|
||||
viewTabs model =
|
||||
case model.feedTab of
|
||||
viewTabs : Session -> FeedTab -> Html Msg
|
||||
viewTabs session feedTab =
|
||||
case feedTab of
|
||||
YourFeed cred ->
|
||||
Feed.viewTabs [] (yourFeed cred) [ globalFeed ]
|
||||
|
||||
GlobalFeed ->
|
||||
let
|
||||
otherTabs =
|
||||
case Session.cred model.session of
|
||||
case Session.cred session of
|
||||
Just cred ->
|
||||
[ yourFeed cred ]
|
||||
|
||||
@@ -181,7 +176,7 @@ viewTabs model =
|
||||
TagFeed tag ->
|
||||
let
|
||||
otherTabs =
|
||||
case Session.cred model.session of
|
||||
case Session.cred session of
|
||||
Just cred ->
|
||||
[ yourFeed cred, globalFeed ]
|
||||
|
||||
|
||||
@@ -61,24 +61,21 @@ init session username =
|
||||
let
|
||||
maybeCred =
|
||||
Session.cred session
|
||||
|
||||
model =
|
||||
{ session = session
|
||||
, timeZone = Time.utc
|
||||
, errors = []
|
||||
, feedTab = defaultFeedTab
|
||||
, feedPage = 1
|
||||
, author = Loading username
|
||||
, feed = Loading username
|
||||
}
|
||||
in
|
||||
( model
|
||||
( { session = session
|
||||
, timeZone = Time.utc
|
||||
, errors = []
|
||||
, feedTab = defaultFeedTab
|
||||
, feedPage = 1
|
||||
, author = Loading username
|
||||
, feed = Loading username
|
||||
}
|
||||
, Cmd.batch
|
||||
[ Author.fetch username maybeCred
|
||||
|> Http.toTask
|
||||
|> Task.mapError (Tuple.pair username)
|
||||
|> Task.attempt CompletedAuthorLoad
|
||||
, fetchFeed model defaultFeedTab 1
|
||||
, fetchFeed session username defaultFeedTab 1
|
||||
, Task.perform GotTimeZone Time.here
|
||||
, Task.perform (\_ -> PassedSlowLoadThreshold) Loading.slowThreshold
|
||||
]
|
||||
@@ -110,19 +107,11 @@ defaultFeedTab =
|
||||
-- HTTP
|
||||
|
||||
|
||||
{-| 👉 TODO: refactor this to accept narrower types than the entire Model.
|
||||
|
||||
💡 HINT: It may end up with multiple arguments!
|
||||
|
||||
-}
|
||||
fetchFeed : Model -> FeedTab -> Int -> Cmd Msg
|
||||
fetchFeed model feedTabs page =
|
||||
fetchFeed : Session -> Username -> FeedTab -> Int -> Cmd Msg
|
||||
fetchFeed session username feedTabs page =
|
||||
let
|
||||
username =
|
||||
currentUsername model
|
||||
|
||||
maybeCred =
|
||||
Session.cred model.session
|
||||
Session.cred session
|
||||
|
||||
( extraParamName, extraParamVal ) =
|
||||
case feedTabs of
|
||||
@@ -138,7 +127,7 @@ fetchFeed model feedTabs page =
|
||||
|> HttpBuilder.withQueryParam extraParamName extraParamVal
|
||||
|> Cred.addHeaderIfAvailable maybeCred
|
||||
|> PaginatedList.fromRequestBuilder articlesPerPage page
|
||||
|> Task.map (Feed.init model.session)
|
||||
|> Task.map (Feed.init session)
|
||||
|> Task.mapError (Tuple.pair username)
|
||||
|> Task.attempt CompletedFeedLoad
|
||||
|
||||
@@ -351,12 +340,12 @@ update msg model =
|
||||
|
||||
ClickedTab tab ->
|
||||
( { model | feedTab = tab }
|
||||
, fetchFeed model tab 1
|
||||
, fetchFeed model.session (currentUsername model) tab 1
|
||||
)
|
||||
|
||||
ClickedFeedPage page ->
|
||||
( { model | feedPage = page }
|
||||
, fetchFeed model model.feedTab page
|
||||
, fetchFeed model.session (currentUsername model) model.feedTab page
|
||||
)
|
||||
|
||||
CompletedFollowChange (Ok newAuthor) ->
|
||||
|
||||
@@ -100,7 +100,7 @@ view : Model -> { title : String, content : Html Msg }
|
||||
view model =
|
||||
let
|
||||
form =
|
||||
viewForm model
|
||||
viewForm (Session.cred model.session) model.form
|
||||
in
|
||||
{ title = "Settings"
|
||||
, content =
|
||||
@@ -124,16 +124,9 @@ view model =
|
||||
}
|
||||
|
||||
|
||||
{-| 👉 TODO refactor this to accept narrower types than the entire Model.
|
||||
💡 HINT: It may end up with multiple arguments!
|
||||
-}
|
||||
viewForm : Model -> Html Msg
|
||||
viewForm model =
|
||||
let
|
||||
form =
|
||||
model.form
|
||||
in
|
||||
case Session.cred model.session of
|
||||
viewForm : Maybe Cred -> Form -> Html Msg
|
||||
viewForm maybeCred form =
|
||||
case maybeCred of
|
||||
Nothing ->
|
||||
text ""
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -109,7 +109,7 @@ view model =
|
||||
]
|
||||
, Feed.viewArticles model.timeZone feed
|
||||
|> List.map (Html.map GotFeedMsg)
|
||||
, [ viewPagination (Feed.articles feed) ]
|
||||
, [ PaginatedList.view ClickedFeedPage (Feed.articles feed) ]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -155,46 +155,6 @@ viewBanner =
|
||||
|
||||
|
||||
|
||||
-- PAGINATION
|
||||
|
||||
|
||||
{-| 👉 TODO: Relocate `viewPagination` into `PaginatedList.view` and make it reusable,
|
||||
then refactor both Page.Home and Page.Profile to use it!
|
||||
|
||||
💡 HINT: Make `PaginatedList.view` return `Html msg` instead of `Html Msg`.
|
||||
(You'll need to introduce at least one extra argument for this to work.)
|
||||
|
||||
-}
|
||||
viewPagination : PaginatedList (Article Preview) -> Html Msg
|
||||
viewPagination list =
|
||||
let
|
||||
viewPageLink currentPage =
|
||||
pageLink currentPage (currentPage == page list)
|
||||
in
|
||||
if total list > 1 then
|
||||
List.range 1 (total list)
|
||||
|> List.map viewPageLink
|
||||
|> ul [ class "pagination" ]
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
|
||||
|
||||
pageLink : Int -> Bool -> Html Msg
|
||||
pageLink targetPage isActive =
|
||||
li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ]
|
||||
[ a
|
||||
[ class "page-link"
|
||||
, onClick (ClickedFeedPage targetPage)
|
||||
|
||||
-- The RealWorld CSS requires an href to work properly.
|
||||
, href ""
|
||||
]
|
||||
[ text (String.fromInt targetPage) ]
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- TABS
|
||||
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ view model =
|
||||
[ [ viewTabs model.feedTab ]
|
||||
, Feed.viewArticles model.timeZone feed
|
||||
|> List.map (Html.map GotFeedMsg)
|
||||
, [ viewPagination (Feed.articles feed) ]
|
||||
, [ PaginatedList.view ClickedFeedPage (Feed.articles feed) ]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -246,46 +246,6 @@ view model =
|
||||
|
||||
|
||||
|
||||
-- PAGINATION
|
||||
|
||||
|
||||
{-| 👉 TODO: Relocate `viewPagination` into `PaginatedList.view` and make it reusable,
|
||||
then refactor both Page.Home and Page.Profile to use it!
|
||||
|
||||
💡 HINT: Make `PaginatedList.view` return `Html msg` instead of `Html Msg`.
|
||||
(You'll need to introduce at least one extra argument for this to work.)
|
||||
|
||||
-}
|
||||
viewPagination : PaginatedList (Article Preview) -> Html Msg
|
||||
viewPagination list =
|
||||
let
|
||||
viewPageLink currentPage =
|
||||
pageLink currentPage (currentPage == page list)
|
||||
in
|
||||
if total list > 1 then
|
||||
List.range 1 (total list)
|
||||
|> List.map viewPageLink
|
||||
|> ul [ class "pagination" ]
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
|
||||
|
||||
pageLink : Int -> Bool -> Html Msg
|
||||
pageLink targetPage isActive =
|
||||
li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ]
|
||||
[ a
|
||||
[ class "page-link"
|
||||
, onClick (ClickedFeedPage targetPage)
|
||||
|
||||
-- The RealWorld CSS requires an href to work properly.
|
||||
, href ""
|
||||
]
|
||||
[ text (String.fromInt targetPage) ]
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- PAGE TITLE
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values)
|
||||
module PaginatedList exposing (PaginatedList, fromList, fromRequestBuilder, map, page, total, values, view)
|
||||
|
||||
import Article exposing (Article, Preview)
|
||||
import Html exposing (Html, a, li, text, ul)
|
||||
import Html.Attributes exposing (class, classList, href)
|
||||
import Html.Events exposing (onClick)
|
||||
@@ -87,3 +88,32 @@ fromRequestBuilder resultsPerPage pageNumber builder =
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : (Int -> msg) -> PaginatedList (Article Preview) -> Html msg
|
||||
view toMsg list =
|
||||
let
|
||||
viewPageLink currentPage =
|
||||
pageLink toMsg currentPage (currentPage == page list)
|
||||
in
|
||||
if total list > 1 then
|
||||
List.range 1 (total list)
|
||||
|> List.map viewPageLink
|
||||
|> ul [ class "pagination" ]
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
|
||||
|
||||
pageLink : (Int -> msg) -> Int -> Bool -> Html msg
|
||||
pageLink toMsg targetPage isActive =
|
||||
li [ classList [ ( "page-item", True ), ( "active", isActive ) ] ]
|
||||
[ a
|
||||
[ class "page-link"
|
||||
, onClick (toMsg targetPage)
|
||||
|
||||
-- The RealWorld CSS requires an href to work properly.
|
||||
, href ""
|
||||
]
|
||||
[ text (String.fromInt targetPage) ]
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -155,46 +155,27 @@ viewBanner =
|
||||
-- TABS
|
||||
|
||||
|
||||
{-| TODO: Have viewTabs render all the tabs, using `activeTab` as the
|
||||
single source of truth for their state.
|
||||
|
||||
The specification for how the tabs work is:
|
||||
|
||||
1. If the user is logged in, render `yourFeed` as the first tab. Examples:
|
||||
|
||||
"Your Feed" "Global Feed"
|
||||
"Your Feed" "Global Feed" "#dragons"
|
||||
|
||||
2. If the user is NOT logged in, do not render `yourFeed` at all. Examples:
|
||||
|
||||
"Global Feed"
|
||||
"Global Feed" "#dragons"
|
||||
|
||||
3. If the active tab is a `TagFeed`, render that tab last. Show the tag it contains with a "#" in front.
|
||||
|
||||
"Global Feed" "#dragons"
|
||||
"Your Feed" "Global Feed" "#dragons"
|
||||
|
||||
3. If the active tab is NOT a `TagFeed`, do not render a tag tab at all.
|
||||
|
||||
"Your Feed" "Global Feed"
|
||||
"Global Feed"
|
||||
|
||||
💡 HINT: The 4 declarations after `viewTabs` may be helpful!
|
||||
|
||||
-}
|
||||
viewTabs : Bool -> FeedTab -> Html Msg
|
||||
viewTabs isLoggedIn activeTab =
|
||||
ul [ class "nav nav-pills outline-active" ] <|
|
||||
case activeTab of
|
||||
YourFeed ->
|
||||
[]
|
||||
List.singleton <|
|
||||
case activeTab of
|
||||
YourFeed ->
|
||||
tabBar [] yourFeed [ globalFeed ]
|
||||
|
||||
GlobalFeed ->
|
||||
[]
|
||||
GlobalFeed ->
|
||||
if isLoggedIn then
|
||||
tabBar [ yourFeed ] globalFeed []
|
||||
|
||||
TagFeed tagName ->
|
||||
[]
|
||||
else
|
||||
tabBar [] globalFeed []
|
||||
|
||||
TagFeed tagName ->
|
||||
if isLoggedIn then
|
||||
tabBar [ yourFeed, globalFeed ] (tagFeed tagName) []
|
||||
|
||||
else
|
||||
tabBar [ globalFeed ] (tagFeed tagName) []
|
||||
|
||||
|
||||
tabBar :
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -206,17 +206,14 @@ toggleFollowButton txt extraClasses msgWhenClicked uname =
|
||||
|
||||
decoder : Maybe Cred -> Decoder Author
|
||||
decoder maybeCred =
|
||||
{- 👉 TODO: Use this `Profile` and `Username` to decode an `Author`!
|
||||
|
||||
💡 HINT: `decoderHelp` will help here, but slightly altering its type may make things easier...
|
||||
-}
|
||||
Decode.succeed "..."
|
||||
Decode.succeed Tuple.pair
|
||||
|> custom Profile.decoder
|
||||
|> required "username" Username.decoder
|
||||
|> Decode.andThen (decoderHelp maybeCred)
|
||||
|
||||
|
||||
decoderHelp : Maybe Cred -> Profile -> Username -> Decoder Author
|
||||
decoderHelp maybeCred prof uname =
|
||||
decoderHelp : Maybe Cred -> ( Profile, Username ) -> Decoder Author
|
||||
decoderHelp maybeCred ( prof, uname ) =
|
||||
case maybeCred of
|
||||
Nothing ->
|
||||
-- If you're logged out, you can't be following anyone!
|
||||
|
||||
@@ -24,17 +24,18 @@ view timeZone timestamp =
|
||||
-}
|
||||
iso8601Decoder : Decoder Time.Posix
|
||||
iso8601Decoder =
|
||||
{- 👉 TODO: Use the following function to decode this Time.Posix value:
|
||||
Decode.string
|
||||
|> Decode.andThen decoderHelp
|
||||
|
||||
|
||||
Iso8601.toTime : String -> Result (List DeadEnd) Time.Posix
|
||||
decoderHelp : String -> Decoder Time.Posix
|
||||
decoderHelp str =
|
||||
case Iso8601.toTime str of
|
||||
Ok time ->
|
||||
Decode.succeed time
|
||||
|
||||
|
||||
❕ NOTE: You can disregard the (List DeadEnd) here. No need to use it to complete this exercise!
|
||||
|
||||
💡 HINT: Decode.andThen will be useful here.
|
||||
-}
|
||||
"..."
|
||||
Err _ ->
|
||||
Decode.fail ("Invalid ISO-8601 timestamp: " ++ str)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -34,14 +34,11 @@ parser =
|
||||
, Parser.map Login (s "login")
|
||||
, Parser.map Logout (s "logout")
|
||||
, Parser.map Profile (s "profile" </> Username.urlParser)
|
||||
|
||||
-- 👉 TODO /settings → Settings
|
||||
-- 👉 TODO /register → Register
|
||||
-- 👉 TODO /article/[slug] → Article [slug]
|
||||
-- 👉 TODO /editor → NewArticle
|
||||
-- 👉 TODO /editor/[slug] → EditArticle [slug]
|
||||
--
|
||||
-- 💡 HINT: Article and EditArticle work similarly to how Profile works.
|
||||
, Parser.map Settings (s "settings")
|
||||
, Parser.map Register (s "settings")
|
||||
, Parser.map Article (s "article" </> Slug.urlParser)
|
||||
, Parser.map EditArticle (s "editor" </> Slug.urlParser)
|
||||
, Parser.map NewArticle (s "editor")
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -5,25 +5,11 @@ import Html.Attributes exposing (..)
|
||||
|
||||
|
||||
banner =
|
||||
{- 👉 TODO: Add a logo and tagline to this banner, so its structure becomes:
|
||||
|
||||
<div class="banner">
|
||||
<div class="container">
|
||||
|
||||
<h1 class="logo-font">conduit</h1>
|
||||
|
||||
<p>A place to share your knowledge.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
💡 HINT 1: the <div class="row"> above is an element with 2 child nodes.
|
||||
|
||||
💡 HINT 2: the <div class="feed-toggle"> below is an element with text.
|
||||
-}
|
||||
div [ class "banner" ]
|
||||
[ div [ class "container" ]
|
||||
[ text "👉 TODO: Put the <h1> here instead of this text, then add the <p> right after the <h1>" ]
|
||||
[ h1 [ class "logo-font" ] [ text "conduit" ]
|
||||
, p [] [ text "A place to share your knowledge." ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -33,7 +19,7 @@ feed =
|
||||
|
||||
main =
|
||||
div [ class "home-page" ]
|
||||
[ div [] [ text "👉 TODO: Replace this <div> with the banner" ]
|
||||
[ banner
|
||||
, div [ class "container page" ]
|
||||
[ div [ class "row" ]
|
||||
[ div [ class "col-md-9" ] [ feed ]
|
||||
|
||||
@@ -7,18 +7,13 @@ import Html.Attributes exposing (..)
|
||||
viewTags tags =
|
||||
let
|
||||
renderedTags =
|
||||
-- 👉 TODO: use `List.map` and `viewTag` to render the tags
|
||||
[]
|
||||
List.map viewTag tags
|
||||
in
|
||||
div [ class "tag-list" ] renderedTags
|
||||
|
||||
|
||||
viewTag tagName =
|
||||
{- 👉 TODO: render something like this:
|
||||
|
||||
<button class="tag-pill tag-default">tag name goes here</button>
|
||||
-}
|
||||
button [] []
|
||||
button [ class "tag-pill tag-default" ] [ text tagName ]
|
||||
|
||||
|
||||
main =
|
||||
@@ -34,9 +29,7 @@ main =
|
||||
, div [ class "col-md-3" ]
|
||||
[ div [ class "sidebar" ]
|
||||
[ p [] [ text "Popular Tags" ]
|
||||
|
||||
-- 👉 TODO: instead of passing [] to viewTags, pass the actual tags
|
||||
, viewTags []
|
||||
, viewTags tags
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/json": "1.0.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0"
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
|
||||
@@ -25,15 +25,11 @@ initialModel =
|
||||
|
||||
|
||||
update msg model =
|
||||
{- 👉 TODO: If `msg.description` is "ClickedTag", then
|
||||
set the model's `selectedTag` field to be `msg.data`
|
||||
if msg.description == "ClickedTag" then
|
||||
{ model | selectedTag = msg.data }
|
||||
|
||||
💡 HINT: record update syntax looks like this:
|
||||
|
||||
{ model | foo = bar }
|
||||
|
||||
-}
|
||||
model
|
||||
else
|
||||
model
|
||||
|
||||
|
||||
|
||||
@@ -42,16 +38,8 @@ update msg model =
|
||||
|
||||
view model =
|
||||
let
|
||||
{- 👉 TODO: Filter the articles down to only the ones
|
||||
that include the currently selected tag.
|
||||
|
||||
💡 HINT: Replace `True` below with something involving
|
||||
`List.member`, `article.tags`, and `model.selectedTag`
|
||||
|
||||
Docs for List.member: http://package.elm-lang.org/packages/elm-lang/core/latest/List#member
|
||||
-}
|
||||
articles =
|
||||
List.filter (\article -> True)
|
||||
List.filter (\article -> List.member model.selectedTag article.tags)
|
||||
model.allArticles
|
||||
|
||||
feed =
|
||||
@@ -101,17 +89,7 @@ viewTag selectedTagName tagName =
|
||||
in
|
||||
button
|
||||
[ class ("tag-pill " ++ otherClass)
|
||||
|
||||
{- 👉 TODO: Add an `onClick` handler which sends a msg
|
||||
that our `update` function above will use
|
||||
to set the currently selected tag to `tagName`.
|
||||
|
||||
💡 HINT: It should look something like this:
|
||||
|
||||
, onClick { description = … , data = … }
|
||||
|
||||
👆 Don't forget to add a comma before `onClick`!
|
||||
-}
|
||||
, onClick { description = "ClickedTag", data = tagName }
|
||||
]
|
||||
[ text tagName ]
|
||||
|
||||
|
||||
@@ -14,27 +14,20 @@ import Html.Events exposing (onClick)
|
||||
type alias Model =
|
||||
{ tags : List String
|
||||
, selectedTag : String
|
||||
|
||||
{- 👉 TODO: change this `allArticles` annotation to the following:
|
||||
|
||||
allArticles : List Article
|
||||
|
||||
|
||||
💡 HINT: You'll need to move the existing annotation to a `type alias`.
|
||||
-}
|
||||
, allArticles :
|
||||
List
|
||||
{ title : String
|
||||
, description : String
|
||||
, body : String
|
||||
, tags : List String
|
||||
, slug : String
|
||||
}
|
||||
, allArticles : List Article
|
||||
}
|
||||
|
||||
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `initialModel`
|
||||
-}
|
||||
type alias Article =
|
||||
{ title : String
|
||||
, description : String
|
||||
, body : String
|
||||
, tags : List String
|
||||
, slug : String
|
||||
}
|
||||
|
||||
|
||||
initialModel : Model
|
||||
initialModel =
|
||||
{ tags = Article.tags
|
||||
, selectedTag = "elm"
|
||||
@@ -52,8 +45,7 @@ type alias Msg =
|
||||
}
|
||||
|
||||
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `update`
|
||||
-}
|
||||
update : Msg -> Model -> Model
|
||||
update msg model =
|
||||
if msg.description == "ClickedTag" then
|
||||
{ model | selectedTag = msg.data }
|
||||
@@ -66,8 +58,7 @@ update msg model =
|
||||
-- VIEW
|
||||
|
||||
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `view`
|
||||
-}
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
let
|
||||
articles =
|
||||
@@ -93,8 +84,7 @@ view model =
|
||||
]
|
||||
|
||||
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `view`
|
||||
-}
|
||||
viewArticle : Article -> Html Msg
|
||||
viewArticle article =
|
||||
div [ class "article-preview" ]
|
||||
[ h1 [] [ text article.title ]
|
||||
@@ -103,8 +93,7 @@ viewArticle article =
|
||||
]
|
||||
|
||||
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewBanner`
|
||||
-}
|
||||
viewBanner : Html Msg
|
||||
viewBanner =
|
||||
div [ class "banner" ]
|
||||
[ div [ class "container" ]
|
||||
@@ -114,8 +103,7 @@ viewBanner =
|
||||
]
|
||||
|
||||
|
||||
{-| 👉 TODO: Replace this comment with a type annotation for `viewTag`
|
||||
-}
|
||||
viewTag : String -> String -> Html Msg
|
||||
viewTag selectedTagName tagName =
|
||||
let
|
||||
otherClass =
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -86,11 +86,7 @@ viewForm form =
|
||||
[ input
|
||||
[ class "form-control form-control-lg"
|
||||
, placeholder "Username"
|
||||
|
||||
{- 👉 TODO: when the user inputs a username, update it in the Model.
|
||||
|
||||
💡 HINT: Look at how the Email input below does this. 👇
|
||||
-}
|
||||
, onInput EnteredUsername
|
||||
, value form.username
|
||||
]
|
||||
[]
|
||||
@@ -141,6 +137,7 @@ type Msg
|
||||
= SubmittedForm
|
||||
| EnteredEmail String
|
||||
| EnteredPassword String
|
||||
| EnteredUsername String
|
||||
| CompletedRegister (Result Http.Error Viewer)
|
||||
| GotSession Session
|
||||
|
||||
@@ -154,6 +151,9 @@ update msg model =
|
||||
EnteredPassword password ->
|
||||
updateForm (\form -> { form | password = password }) model
|
||||
|
||||
EnteredUsername username ->
|
||||
updateForm (\form -> { form | username = username }) model
|
||||
|
||||
SubmittedForm ->
|
||||
case validate model.form of
|
||||
Ok validForm ->
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -40,12 +40,12 @@ src (Avatar maybeUrl) =
|
||||
|
||||
resolveAvatarUrl : Maybe String -> String
|
||||
resolveAvatarUrl maybeUrl =
|
||||
{- 👉 TODO #1 of 2: return the user's avatar from maybeUrl, if maybeUrl actually
|
||||
contains one. If maybeUrl is Nothing, return this URL instead:
|
||||
case maybeUrl of
|
||||
Just url ->
|
||||
url
|
||||
|
||||
https://static.productionready.io/images/smiley-cyrus.jpg
|
||||
-}
|
||||
""
|
||||
Nothing ->
|
||||
"https://static.productionready.io/images/smiley-cyrus.jpg"
|
||||
|
||||
|
||||
encode : Avatar -> Value
|
||||
|
||||
@@ -572,6 +572,7 @@ toTagList tagString =
|
||||
-}
|
||||
String.split " " tagString
|
||||
|> List.map String.trim
|
||||
|> List.filter (\str -> str /= "")
|
||||
|
||||
|
||||
edit : Slug -> TrimmedForm -> Cred -> Http.Request (Article Full)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -182,26 +182,12 @@ type alias Metadata =
|
||||
|
||||
metadataDecoder : Decoder Metadata
|
||||
metadataDecoder =
|
||||
{- 👉 TODO: replace the calls to `hardcoded` with calls to `required`
|
||||
in order to decode these fields:
|
||||
|
||||
--- "description" -------> description : String
|
||||
--- "title" -------------> title : String
|
||||
--- "tagList" -----------> tags : List String
|
||||
--- "favorited" ---------> favorited : Bool
|
||||
--- "favoritesCount" ----> favoritesCount : Int
|
||||
|
||||
Once this is done, the articles in the feed should look normal again.
|
||||
|
||||
💡 HINT: Order matters! These must be decoded in the same order
|
||||
as the order of the fields in `type alias Metadata` above. ☝️
|
||||
-}
|
||||
Decode.succeed Metadata
|
||||
|> hardcoded "(needs decoding!)"
|
||||
|> hardcoded "(needs decoding!)"
|
||||
|> hardcoded []
|
||||
|> hardcoded False
|
||||
|> hardcoded 0
|
||||
|> required "description" string
|
||||
|> required "title" string
|
||||
|> required "tagList" (list string)
|
||||
|> required "favorited" bool
|
||||
|> required "favoritesCount" int
|
||||
|> required "createdAt" Timestamp.iso8601Decoder
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1"
|
||||
"rtfeldman/elm-iso8601-date-strings": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
|
||||
@@ -155,34 +155,13 @@ update msg model =
|
||||
responseDecoder =
|
||||
Decode.field "user" Viewer.decoder
|
||||
|
||||
{- 👉 TODO: Create a Http.Request value that represents
|
||||
a POST request to "/api/users"
|
||||
|
||||
💡 HINT 1: Documentation for `Http.post` is here:
|
||||
|
||||
http://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#post
|
||||
|
||||
💡 HINT 2: Look at the values defined above in this
|
||||
let-expression. What are their types? What are the types the
|
||||
`Http.post` function is looking for?
|
||||
-}
|
||||
request : Http.Request Viewer
|
||||
request =
|
||||
Debug.todo "Call Http.post to represent a POST to /api/users/login"
|
||||
Http.post "/api/users" requestBody responseDecoder
|
||||
|
||||
{- 👉 TODO: Use Http.send to turn the request we just defined
|
||||
into a Cmd for `update` to execute.
|
||||
|
||||
💡 HINT 1: Documentation for `Http.send` is here:
|
||||
|
||||
http://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#send
|
||||
|
||||
💡 HINT 2: The `CompletedRegister` variant defined in `type Msg`
|
||||
will be useful here!
|
||||
-}
|
||||
cmd : Cmd Msg
|
||||
cmd =
|
||||
Cmd.none
|
||||
Http.send CompletedRegister request
|
||||
in
|
||||
( { model | problems = [] }, cmd )
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ login newViewer =
|
||||
Viewer.encode newViewer
|
||||
|> Encode.encode 0
|
||||
|> Just
|
||||
|> sendSessionToJavaScript
|
||||
|> storeSession
|
||||
|
||||
|
||||
|
||||
@@ -81,24 +81,10 @@ login newViewer =
|
||||
|
||||
logout : Cmd msg
|
||||
logout =
|
||||
sendSessionToJavaScript Nothing
|
||||
storeSession Nothing
|
||||
|
||||
|
||||
{-| 👉 TODO 1 of 2: Replace this do-nothing function with a port that sends the
|
||||
authentication token to JavaScript.
|
||||
|
||||
💡 HINT 1: When you convert it to a port, the port's name _must_ match
|
||||
the name JavaScript expects in `intro/server/public/index.html`.
|
||||
That name is not `sendSessionToJavaScript`, so you will need to
|
||||
rename it to match what JS expects!
|
||||
|
||||
💡 HINT 2: After you rename it, some code in this file will break because
|
||||
it was depending on the old name. Follow the compiler errors to fix them!
|
||||
|
||||
-}
|
||||
sendSessionToJavaScript : Maybe String -> Cmd msg
|
||||
sendSessionToJavaScript maybeAuthenticationToken =
|
||||
Cmd.none
|
||||
port storeSession : Maybe String -> Cmd msg
|
||||
|
||||
|
||||
|
||||
@@ -107,24 +93,10 @@ sendSessionToJavaScript maybeAuthenticationToken =
|
||||
|
||||
changes : (Session -> msg) -> Nav.Key -> Sub msg
|
||||
changes toMsg key =
|
||||
receiveSessionFromJavaScript (\val -> toMsg (decode key val))
|
||||
onSessionChange (\val -> toMsg (decode key val))
|
||||
|
||||
|
||||
{-| 👉 TODO 2 of 2: Replace this do-nothing function with a port that receives the
|
||||
authentication token from JavaScript.
|
||||
|
||||
💡 HINT 1: When you convert it to a port, the port's name _must_ match
|
||||
the name JavaScript expects in `intro/server/public/index.html`.
|
||||
That name is not `receiveSessionFromJavaScript`, so you will need to
|
||||
rename it to match what JS expects!
|
||||
|
||||
💡 HINT 2: After you rename it, some code in this file will break because
|
||||
it was depending on the old name. Follow the compiler errors to fix them!
|
||||
|
||||
-}
|
||||
receiveSessionFromJavaScript : (Value -> msg) -> Sub msg
|
||||
receiveSessionFromJavaScript toMsg =
|
||||
Sub.none
|
||||
port onSessionChange : (Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
decode : Nav.Key -> Value -> Session
|
||||
|
||||
Reference in New Issue
Block a user