From 30b37a289b8583d85c938c25b14fbc60ae6a4a43 Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 1 Mar 2024 14:33:32 +0000 Subject: [PATCH 1/8] install Feliz.UseElmish --- package-lock.json | 17 ++++++++++++++++- package.json | 3 ++- paket.dependencies | 1 + paket.lock | 3 +++ src/Client/paket.references | 3 ++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9af1b52..1887c161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,8 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", - "sweetalert2": "^8.19.1" + "sweetalert2": "^8.19.1", + "use-sync-external-store": "^1.2.0" }, "devDependencies": { "@types/node": "^20.9.1", @@ -3135,6 +3136,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5565,6 +5574,12 @@ "picocolors": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index dadd92ab..355da777 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", - "sweetalert2": "^8.19.1" + "sweetalert2": "^8.19.1", + "use-sync-external-store": "^1.2.0" }, "devDependencies": { "@types/node": "^20.9.1", diff --git a/paket.dependencies b/paket.dependencies index cf6b7fc0..d207ec57 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -8,6 +8,7 @@ nuget Azure.Storage.Blobs nuget Elmish.SweetAlert nuget Feliz.DaisyUI nuget Feliz.Router +nuget Feliz.UseElmish nuget FSharp.Core ~> 8 nuget Fable.Remoting.Giraffe ~> 5 nuget FSToolkit.ErrorHandling diff --git a/paket.lock b/paket.lock index c21122d9..948d8305 100644 --- a/paket.lock +++ b/paket.lock @@ -168,6 +168,9 @@ NUGET Fable.Elmish (>= 4.0) Feliz (>= 2.3) FSharp.Core (>= 4.7.2) + Feliz.UseElmish (2.5) + Fable.Elmish (>= 4.0) + FSharp.Core (>= 4.7.2) FParsec (1.1.1) FSharp.Core (>= 4.3.4) FSharp.Control.Reactive (5.0.5) diff --git a/src/Client/paket.references b/src/Client/paket.references index 669c154f..a41288d9 100644 --- a/src/Client/paket.references +++ b/src/Client/paket.references @@ -10,4 +10,5 @@ Feliz.DaisyUI Feliz.Router FSToolkit.ErrorHandling Elmish.SweetAlert -Fable.SimpleJson \ No newline at end of file +Fable.SimpleJson +Feliz.UseElmish \ No newline at end of file From 53c22740649bc5933e10cc6e3a727074c896040a Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 1 Mar 2024 14:45:09 +0000 Subject: [PATCH 2/8] Use UseElmish for the home page --- src/Client/Index.fs | 27 +++++---------------------- src/Client/pages/Home.fs | 6 +++++- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/Client/Index.fs b/src/Client/Index.fs index 5e7fc662..2985c425 100644 --- a/src/Client/Index.fs +++ b/src/Client/Index.fs @@ -10,7 +10,7 @@ open Page open Shared type PageTab = - | Home of Home.Model + | Home | Login of Login.Model | Wishlist of WishList.Model | NotFound @@ -22,7 +22,6 @@ type User = type Model = { Page: PageTab; User: User } type Msg = - | HomePageMsg of Home.Msg | LoginPageMsg of Login.Msg | WishlistMsg of WishList.Msg | UrlChanged of string list @@ -45,15 +44,8 @@ let guestApi = let initFromUrl model url = match url with | [] -> - let homeModel, homeMsg = Home.init guestApi - - let model = { - Page = Home homeModel - User = model.User - } - - let cmd = homeMsg |> Cmd.map HomePageMsg - model, cmd + let model = { Page = Home; User = model.User } + model, Cmd.none | [ "login" ] -> let loginModel, loginMsg = Login.init () @@ -81,21 +73,12 @@ let initFromUrl model url = | _ -> { Page = NotFound; User = model.User }, Cmd.none let init () = - let model, _ = Home.init guestApi let user = Session.loadUser () |> Option.map User |> Option.defaultValue Guest - Router.currentUrl () |> initFromUrl { Page = Home model; User = user } + Router.currentUrl () |> initFromUrl { Page = Home; User = user } let update msg model = match model.Page, msg with - | Home homeModel, HomePageMsg homeMsg -> - let newModel, cmd = Home.update homeMsg homeModel - - { - Page = Home newModel - User = model.User - }, - cmd | Login loginModel, LoginPageMsg loginMsg -> let user = match loginMsg with @@ -177,7 +160,7 @@ let view model dispatch = prop.className "overflow-y-auto" prop.children [ match model.Page with - | Home homeModel -> Home.view homeModel (HomePageMsg >> dispatch) + | Home -> Home.View guestApi | Login loginModel -> Login.view loginModel (LoginPageMsg >> dispatch) | Wishlist wishlistModel -> WishList.view wishlistModel (WishlistMsg >> dispatch) | NotFound -> Html.div [ prop.text "Not Found" ] diff --git a/src/Client/pages/Home.fs b/src/Client/pages/Home.fs index 3d92d3f8..2a1ea1a4 100644 --- a/src/Client/pages/Home.fs +++ b/src/Client/pages/Home.fs @@ -2,6 +2,7 @@ module Page.Home open Elmish open Feliz.DaisyUI +open Feliz.UseElmish open Shared type Model = { Books: Book seq } @@ -43,7 +44,10 @@ let bookRow book = ] ] -let view model dispatch = +[] +let View api = + let model, dispatch = React.useElmish (init api, update, [||]) + Html.div [ prop.className "overflow-y-auto" prop.children [ From 046cc03ee604d7a6031afd65bb7bc4bb8a1699c9 Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 1 Mar 2024 15:46:02 +0000 Subject: [PATCH 3/8] Use UseElmish for Login page --- src/Client/Index.fs | 24 ++++-------------------- src/Client/pages/Login.fs | 6 +++++- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/Client/Index.fs b/src/Client/Index.fs index 2985c425..d587a24e 100644 --- a/src/Client/Index.fs +++ b/src/Client/Index.fs @@ -11,7 +11,7 @@ open Shared type PageTab = | Home - | Login of Login.Model + | Login | Wishlist of WishList.Model | NotFound @@ -22,7 +22,6 @@ type User = type Model = { Page: PageTab; User: User } type Msg = - | LoginPageMsg of Login.Msg | WishlistMsg of WishList.Msg | UrlChanged of string list | OnSessionChange @@ -47,15 +46,8 @@ let initFromUrl model url = let model = { Page = Home; User = model.User } model, Cmd.none | [ "login" ] -> - let loginModel, loginMsg = Login.init () - - let model = { - Page = Login loginModel - User = model.User - } - - let cmd = loginMsg |> Cmd.map LoginPageMsg - model, cmd + let model = { Page = Login; User = model.User } + model, Cmd.none | [ "wishlist" ] -> match model.User with | User user -> @@ -79,14 +71,6 @@ let init () = let update msg model = match model.Page, msg with - | Login loginModel, LoginPageMsg loginMsg -> - let user = - match loginMsg with - | Login.LoggedIn user -> User user - | _ -> model.User - - let newModel, cmd = Login.update guestApi loginMsg loginModel - { Page = Login newModel; User = user }, cmd |> Cmd.map LoginPageMsg | Wishlist wishlistModel, WishlistMsg wishlistMsg -> let token = match model.User with @@ -161,7 +145,7 @@ let view model dispatch = prop.children [ match model.Page with | Home -> Home.View guestApi - | Login loginModel -> Login.view loginModel (LoginPageMsg >> dispatch) + | Login -> Login.View guestApi | Wishlist wishlistModel -> WishList.view wishlistModel (WishlistMsg >> dispatch) | NotFound -> Html.div [ prop.text "Not Found" ] ] diff --git a/src/Client/pages/Login.fs b/src/Client/pages/Login.fs index 4e04a454..627356a9 100644 --- a/src/Client/pages/Login.fs +++ b/src/Client/pages/Login.fs @@ -4,6 +4,7 @@ open System open Elmish open Feliz.DaisyUI open Feliz.Router +open Feliz.UseElmish open FsToolkit.ErrorHandling open Shared open SAFE @@ -68,7 +69,10 @@ let update (guestApi: IGuestApi) msg model = open Feliz -let view model dispatch = +[] +let View guestApi = + let model, dispatch = React.useElmish (init, (update guestApi), [||]) + Html.div [ prop.className "grid justify-center" prop.children [ From c39736afe66fc8553ebece484af8b8ca154f0edb Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 1 Mar 2024 16:13:36 +0000 Subject: [PATCH 4/8] Use useElmish for wishlist page --- src/Client/Index.fs | 32 ++++---------------------------- src/Client/pages/WishList.fs | 24 +++++++++++++++++++----- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/Client/Index.fs b/src/Client/Index.fs index d587a24e..bcc274f5 100644 --- a/src/Client/Index.fs +++ b/src/Client/Index.fs @@ -12,7 +12,7 @@ open Shared type PageTab = | Home | Login - | Wishlist of WishList.Model + | Wishlist of UserData | NotFound type User = @@ -27,13 +27,6 @@ type Msg = | OnSessionChange | Logout -let wishListApi token = - let bearer = $"Bearer {token}" - - Remoting.createApi () - |> Remoting.withAuthorizationHeader bearer - |> Remoting.withRouteBuilder Route.builder - |> Remoting.buildProxy let guestApi = Remoting.createApi () @@ -51,16 +44,12 @@ let initFromUrl model url = | [ "wishlist" ] -> match model.User with | User user -> - let wishlistModel, wishlistMsg = - WishList.init (wishListApi user.Token) user.UserName - let model = { - Page = Wishlist wishlistModel + Page = Wishlist user User = model.User } - let cmd = wishlistMsg |> Cmd.map WishlistMsg - model, cmd + model, Cmd.none | Guest -> model, Cmd.navigate "login" | _ -> { Page = NotFound; User = model.User }, Cmd.none @@ -71,19 +60,6 @@ let init () = let update msg model = match model.Page, msg with - | Wishlist wishlistModel, WishlistMsg wishlistMsg -> - let token = - match model.User with - | User data -> data.Token - | Guest -> "" - - let newModel, cmd = WishList.update (wishListApi token) wishlistMsg wishlistModel - - { - Page = Wishlist newModel - User = model.User - }, - cmd |> Cmd.map WishlistMsg | NotFound, _ -> { Page = NotFound; User = model.User }, Cmd.none | _, UrlChanged url -> initFromUrl model url | _, Logout -> @@ -146,7 +122,7 @@ let view model dispatch = match model.Page with | Home -> Home.View guestApi | Login -> Login.View guestApi - | Wishlist wishlistModel -> WishList.view wishlistModel (WishlistMsg >> dispatch) + | Wishlist user -> WishList.View user | NotFound -> Html.div [ prop.text "Not Found" ] ] ] diff --git a/src/Client/pages/WishList.fs b/src/Client/pages/WishList.fs index 4c09f93e..3bd10008 100644 --- a/src/Client/pages/WishList.fs +++ b/src/Client/pages/WishList.fs @@ -4,8 +4,18 @@ open System open Elmish open Elmish.SweetAlert open Feliz.DaisyUI +open Feliz.UseElmish open Shared open SAFE +open Fable.Remoting.Client + +let wishListApi token = + let bearer = $"Bearer {token}" + + Remoting.createApi () + |> Remoting.withAuthorizationHeader bearer + |> Remoting.withRouteBuilder Route.builder + |> Remoting.buildProxy type Model = { Wishlist: WishList @@ -26,10 +36,10 @@ type Msg = let alert message alertType = SimpleAlert(message).Type(alertType) |> SweetAlert.Run -let init (wishListApi: IWishListApi) (userName: UserName) = +let init api (user: UserData) = let model = { Wishlist = { - UserName = userName + UserName = user.UserName Books = List.empty } LastResetTime = DateTime.MinValue @@ -38,8 +48,8 @@ let init (wishListApi: IWishListApi) (userName: UserName) = let cmd = Cmd.batch [ - Cmd.OfAsync.either wishListApi.getWishlist userName GotWishlist UnhandledError - Cmd.OfAsync.either wishListApi.getLastResetTime () GotLastRestTime UnhandledError + Cmd.OfAsync.either api.getWishlist user.UserName GotWishlist UnhandledError + Cmd.OfAsync.either api.getLastResetTime () GotLastRestTime UnhandledError ] model, cmd @@ -159,7 +169,11 @@ let table model dispatch = ] ] -let view model dispatch = +[] +let View user = + let api: IWishListApi = wishListApi user.Token + + let model, dispatch = React.useElmish (init api user, update api, [||]) let user = model.Wishlist.UserName.Value let lastReset = model.LastResetTime.ToString("yyyy-MM-dd HH:mm") From 95c29a83c82e7665e713a5e350e40b97582fb74e Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 1 Mar 2024 16:17:49 +0000 Subject: [PATCH 5/8] clean up update --- src/Client/Index.fs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Client/Index.fs b/src/Client/Index.fs index bcc274f5..aa65e227 100644 --- a/src/Client/Index.fs +++ b/src/Client/Index.fs @@ -22,7 +22,6 @@ type User = type Model = { Page: PageTab; User: User } type Msg = - | WishlistMsg of WishList.Msg | UrlChanged of string list | OnSessionChange | Logout @@ -59,13 +58,12 @@ let init () = Router.currentUrl () |> initFromUrl { Page = Home; User = user } let update msg model = - match model.Page, msg with - | NotFound, _ -> { Page = NotFound; User = model.User }, Cmd.none - | _, UrlChanged url -> initFromUrl model url - | _, Logout -> + match msg with + | UrlChanged url -> initFromUrl model url + | Logout -> Session.deleteUser () { model with User = Guest }, Cmd.navigate "" - | _, OnSessionChange -> + | OnSessionChange -> let session = Session.loadUser () let user = session |> Option.map User |> Option.defaultValue Guest @@ -75,7 +73,6 @@ let update msg model = |> Option.defaultValue (Cmd.navigate "login") { model with User = user }, cmd - | _, _ -> model, Cmd.none open Feliz From 14ccb07b0f6a04e3d1f27c07dd87b51929cec4ee Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 8 Mar 2024 11:00:09 +0000 Subject: [PATCH 6/8] reload user from session when navigating --- src/Client/Index.fs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Client/Index.fs b/src/Client/Index.fs index aa65e227..b5077bf9 100644 --- a/src/Client/Index.fs +++ b/src/Client/Index.fs @@ -33,24 +33,27 @@ let guestApi = |> Remoting.buildProxy let initFromUrl model url = + let loggedInUser = + Session.loadUser () |> Option.map User |> Option.defaultValue Guest + match url with | [] -> - let model = { Page = Home; User = model.User } + let model = { Page = Home; User = loggedInUser } model, Cmd.none | [ "login" ] -> - let model = { Page = Login; User = model.User } + let model = { Page = Login; User = loggedInUser } model, Cmd.none | [ "wishlist" ] -> - match model.User with + match loggedInUser with | User user -> let model = { Page = Wishlist user - User = model.User + User = loggedInUser } model, Cmd.none | Guest -> model, Cmd.navigate "login" - | _ -> { Page = NotFound; User = model.User }, Cmd.none + | _ -> { Page = NotFound; User = loggedInUser }, Cmd.none let init () = let user = Session.loadUser () |> Option.map User |> Option.defaultValue Guest From 308f2167056321e2d8611b5a5036cf2ffd1388aa Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 8 Mar 2024 11:03:16 +0000 Subject: [PATCH 7/8] use lazy functions initializing home and wishlist --- src/Client/pages/Home.fs | 2 +- src/Client/pages/WishList.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/pages/Home.fs b/src/Client/pages/Home.fs index 2a1ea1a4..a41860d4 100644 --- a/src/Client/pages/Home.fs +++ b/src/Client/pages/Home.fs @@ -46,7 +46,7 @@ let bookRow book = [] let View api = - let model, dispatch = React.useElmish (init api, update, [||]) + let model, dispatch = React.useElmish ((fun () -> init api), update, [||]) Html.div [ prop.className "overflow-y-auto" diff --git a/src/Client/pages/WishList.fs b/src/Client/pages/WishList.fs index 3bd10008..3b073887 100644 --- a/src/Client/pages/WishList.fs +++ b/src/Client/pages/WishList.fs @@ -173,7 +173,7 @@ let table model dispatch = let View user = let api: IWishListApi = wishListApi user.Token - let model, dispatch = React.useElmish (init api user, update api, [||]) + let model, dispatch = React.useElmish ((fun () -> init api user), update api, [||]) let user = model.Wishlist.UserName.Value let lastReset = model.LastResetTime.ToString("yyyy-MM-dd HH:mm") From adee31f634e86062637bf7a17a62d57784dd065a Mon Sep 17 00:00:00 2001 From: Joost Kaptein Date: Fri, 8 Mar 2024 11:04:43 +0000 Subject: [PATCH 8/8] make naming of "Api" consistent in wishlist --- src/Client/pages/WishList.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Client/pages/WishList.fs b/src/Client/pages/WishList.fs index 3b073887..f00c511d 100644 --- a/src/Client/pages/WishList.fs +++ b/src/Client/pages/WishList.fs @@ -54,7 +54,7 @@ let init api (user: UserData) = model, cmd -let update booksApi msg model = +let update api msg model = match msg with | GotLastRestTime time -> { model with LastResetTime = time }, Cmd.none | GotWishlist wishlist -> { model with Wishlist = wishlist }, Cmd.none @@ -62,7 +62,7 @@ let update booksApi msg model = let userName = model.Wishlist.UserName let cmd = - Cmd.OfAsync.either booksApi.removeBook (userName, title) RemovedBook UnhandledError + Cmd.OfAsync.either api.removeBook (userName, title) RemovedBook UnhandledError model, cmd | RemovedBook title -> @@ -81,7 +81,7 @@ let update booksApi msg model = match model.Wishlist.VerifyNewBookIsNotADuplicate book with | Ok _ -> let userName = model.Wishlist.UserName - model, Cmd.OfAsync.either booksApi.addBook (userName, book) AddedBook UnhandledError + model, Cmd.OfAsync.either api.addBook (userName, book) AddedBook UnhandledError | Error error -> model, Exception(error) |> UnhandledError |> Cmd.ofMsg | NewBook.Cancel, _ -> { model with NewBook = None }, Cmd.none | _, Some newBook ->