diff --git a/package-lock.json b/package-lock.json index ce1e6047..1947709b 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", @@ -3038,6 +3039,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", @@ -5371,6 +5380,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 ebe0439f..eb342f2b 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/Index.fs b/src/Client/Index.fs index 5e7fc662..b5077bf9 100644 --- a/src/Client/Index.fs +++ b/src/Client/Index.fs @@ -10,9 +10,9 @@ open Page open Shared type PageTab = - | Home of Home.Model - | Login of Login.Model - | Wishlist of WishList.Model + | Home + | Login + | Wishlist of UserData | NotFound type User = @@ -22,20 +22,10 @@ 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 | OnSessionChange | Logout -let wishListApi token = - let bearer = $"Bearer {token}" - - Remoting.createApi () - |> Remoting.withAuthorizationHeader bearer - |> Remoting.withRouteBuilder Route.builder - |> Remoting.buildProxy let guestApi = Remoting.createApi () @@ -43,86 +33,40 @@ let guestApi = |> Remoting.buildProxy let initFromUrl model url = + let loggedInUser = + Session.loadUser () |> Option.map User |> Option.defaultValue Guest + 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 = loggedInUser } + 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 = loggedInUser } + model, Cmd.none | [ "wishlist" ] -> - match model.User with + match loggedInUser with | User user -> - let wishlistModel, wishlistMsg = - WishList.init (wishListApi user.Token) user.UserName - let model = { - Page = Wishlist wishlistModel - User = model.User + Page = Wishlist user + User = loggedInUser } - let cmd = wishlistMsg |> Cmd.map WishlistMsg - model, cmd + model, Cmd.none | Guest -> model, Cmd.navigate "login" - | _ -> { Page = NotFound; User = model.User }, Cmd.none + | _ -> { Page = NotFound; User = loggedInUser }, 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 - | 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 - | 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 -> + 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 @@ -132,7 +76,6 @@ let update msg model = |> Option.defaultValue (Cmd.navigate "login") { model with User = user }, cmd - | _, _ -> model, Cmd.none open Feliz @@ -177,9 +120,9 @@ let view model dispatch = prop.className "overflow-y-auto" prop.children [ match model.Page with - | Home homeModel -> Home.view homeModel (HomePageMsg >> dispatch) - | Login loginModel -> Login.view loginModel (LoginPageMsg >> dispatch) - | Wishlist wishlistModel -> WishList.view wishlistModel (WishlistMsg >> dispatch) + | Home -> Home.View guestApi + | Login -> Login.View guestApi + | Wishlist user -> WishList.View user | NotFound -> Html.div [ prop.text "Not Found" ] ] ] diff --git a/src/Client/pages/Home.fs b/src/Client/pages/Home.fs index 3d92d3f8..a41860d4 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 ((fun () -> init api), update, [||]) + Html.div [ prop.className "overflow-y-auto" prop.children [ 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 [ diff --git a/src/Client/pages/WishList.fs b/src/Client/pages/WishList.fs index 4c09f93e..f00c511d 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,13 +48,13 @@ 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 -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 @@ -52,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 -> @@ -71,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 -> @@ -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 ((fun () -> init api user), update api, [||]) let user = model.Wishlist.UserName.Value let lastReset = model.LastResetTime.ToString("yyyy-MM-dd HH:mm") 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