diff --git a/src/Main.elm b/src/Main.elm index 27bab35..ea82c13 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -1,7 +1,5 @@ module Main exposing (main) ---exposing (UrlRequest(..)) - import Browser import Browser.Navigation import Html diff --git a/src/Page/Options.elm b/src/Page/Options.elm index 739693b..ccac5da 100644 --- a/src/Page/Options.elm +++ b/src/Page/Options.elm @@ -43,6 +43,7 @@ import Html.Parser.Util import Json.Decode import Json.Encode import Regex +import Route import Search @@ -102,7 +103,7 @@ update navKey msg model = let ( newModel, newCmd ) = Search.update - "options" + Route.Options navKey subMsg model @@ -116,8 +117,7 @@ update navKey msg model = view : Model -> Html Msg view model = - Search.view - "options" + Search.view { toRoute = Route.Options, categoryName = "options" } "Search NixOS options" model viewSuccess diff --git a/src/Page/Packages.elm b/src/Page/Packages.elm index d0a4c82..a4901fb 100644 --- a/src/Page/Packages.elm +++ b/src/Page/Packages.elm @@ -43,6 +43,7 @@ import Json.Decode import Json.Decode.Pipeline import Json.Encode import Regex +import Route import Search @@ -139,7 +140,7 @@ update navKey msg model = let ( newModel, newCmd ) = Search.update - "packages" + Route.Packages navKey subMsg model @@ -153,8 +154,7 @@ update navKey msg model = view : Model -> Html Msg view model = - Search.view - "packages" + Search.view { toRoute = Route.Packages, categoryName = "packages" } "Search NixOS packages" model viewSuccess diff --git a/src/Route.elm b/src/Route.elm index 29e19ec..64cc209 100644 --- a/src/Route.elm +++ b/src/Route.elm @@ -1,9 +1,11 @@ -module Route exposing (Route(..), fromUrl, href, replaceUrl) +module Route exposing (Route(..), fromUrl, href, replaceUrl, routeToString) import Browser.Navigation import Html import Html.Attributes +import Route.SearchQuery import Url +import Url.Builder exposing (QueryParameter) import Url.Parser exposing (()) import Url.Parser.Query @@ -19,30 +21,32 @@ type Route | Options (Maybe String) (Maybe String) (Maybe String) (Maybe Int) (Maybe Int) (Maybe String) -parser : Url.Parser.Parser (Route -> msg) msg -parser = +parser : Url.Url -> Url.Parser.Parser (Route -> msg) msg +parser url = + let + rawQuery = + Route.SearchQuery.toRawQuery url + + withSearchQuery : (a -> Maybe String -> b) -> a -> b + withSearchQuery f channel = + f channel <| + Maybe.andThen Route.SearchQuery.searchQueryToString <| + Maybe.andThen (Route.SearchQuery.searchString "query") rawQuery + in Url.Parser.oneOf - [ Url.Parser.map - Home - Url.Parser.top - , Url.Parser.map - NotFound - (Url.Parser.s "not-found") - , Url.Parser.map - Packages + [ Url.Parser.map Home Url.Parser.top + , Url.Parser.map NotFound (Url.Parser.s "not-found") + , Url.Parser.map (withSearchQuery Packages) (Url.Parser.s "packages" Url.Parser.Query.string "channel" - Url.Parser.Query.string "query" Url.Parser.Query.string "show" Url.Parser.Query.int "from" Url.Parser.Query.int "size" Url.Parser.Query.string "sort" ) - , Url.Parser.map - Options + , Url.Parser.map (withSearchQuery Options) (Url.Parser.s "options" Url.Parser.Query.string "channel" - Url.Parser.Query.string "query" Url.Parser.Query.string "show" Url.Parser.Query.int "from" Url.Parser.Query.int "size" @@ -71,7 +75,7 @@ fromUrl url = -- This makes it *literally* the path, so we can proceed -- with parsing as if it had been a normal path all along. --{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing } - Url.Parser.parse parser url + Url.Parser.parse (parser url) url @@ -79,41 +83,63 @@ fromUrl url = routeToString : Route -> String -routeToString page = +routeToString = let - ( path, query ) = - routeToPieces page + buildString ( path, query, searchQuery ) = + Route.SearchQuery.absolute path query <| + Maybe.withDefault [] <| + Maybe.map List.singleton searchQuery in - "/" ++ String.join "/" path ++ "?" ++ String.join "&" (List.filterMap Basics.identity query) + buildString << routeToPieces -routeToPieces : Route -> ( List String, List (Maybe String) ) +routeToPieces : Route -> ( List String, List QueryParameter, Maybe ( String, Route.SearchQuery.SearchQuery ) ) routeToPieces page = - case page of - Home -> - ( [], [] ) + let + channelQ = + Maybe.map (Url.Builder.string "channel") - NotFound -> - ( [ "not-found" ], [] ) + queryQ = + Maybe.map (Route.SearchQuery.toSearchQuery "query") - Packages channel query show from size sort -> - ( [ "packages" ] - , [ channel - , query - , show - , Maybe.map String.fromInt from - , Maybe.map String.fromInt size - , sort - ] - ) + showQ = + Maybe.map (Url.Builder.string "show") - Options channel query show from size sort -> - ( [ "options" ] - , [ channel - , query - , show - , Maybe.map String.fromInt from - , Maybe.map String.fromInt size - , sort - ] - ) + fromQ = + Maybe.map (Url.Builder.int "from") + + sizeQ = + Maybe.map (Url.Builder.int "size") + + sortQ = + Maybe.map (Url.Builder.string "sort") + in + (\( path, urlQ, searchQuery ) -> ( path, List.filterMap identity urlQ, searchQuery )) <| + case page of + Home -> + ( [], [], Nothing ) + + NotFound -> + ( [ "not-found" ], [], Nothing ) + + Packages channel query show from size sort -> + ( [ "packages" ] + , [ channelQ channel + , showQ show + , fromQ from + , sizeQ size + , sortQ sort + ] + , queryQ query + ) + + Options channel query show from size sort -> + ( [ "options" ] + , [ channelQ channel + , showQ show + , fromQ from + , sizeQ size + , sortQ sort + ] + , queryQ query + ) diff --git a/src/Route/SearchQuery.elm b/src/Route/SearchQuery.elm new file mode 100644 index 0000000..f2e385b --- /dev/null +++ b/src/Route/SearchQuery.elm @@ -0,0 +1,87 @@ +module Route.SearchQuery exposing + ( RawQuery + , SearchQuery + , absolute + , searchQueryToString + , searchString + , toRawQuery + , toSearchQuery + ) + +import Dict exposing (Dict) +import Url +import Url.Builder + + + +-- RawQuery + + +type RawQuery + = RawQuery (Dict String String) + + +chunk : String -> String -> Maybe ( String, String ) +chunk sep str = + case String.split sep str of + [] -> + Nothing + + [ key ] -> + Just ( key, "" ) + + key :: xs -> + Just ( key, String.join sep xs ) + + +toRawQuery : Url.Url -> Maybe RawQuery +toRawQuery = + Maybe.map (RawQuery << Dict.fromList << List.filterMap (chunk "=") << String.split "&") + << .query + + + +-- SearchQuery + + +{-| This is type safe wrapper for working with search queries in url +-} +type SearchQuery + = SearchQuery String + + +searchString : String -> RawQuery -> Maybe SearchQuery +searchString name (RawQuery dict) = + Maybe.map SearchQuery <| Dict.get name dict + + +searchQueryToString : SearchQuery -> Maybe String +searchQueryToString (SearchQuery str) = + Url.percentDecode <| String.replace "+" "%20" str + + +toSearchQuery : String -> String -> ( String, SearchQuery ) +toSearchQuery name query = + ( name, SearchQuery <| String.replace "%20" "+" <| Url.percentEncode query ) + + +{-| Build absolute URL with support for search query strings +-} +absolute : List String -> List Url.Builder.QueryParameter -> List ( String, SearchQuery ) -> String +absolute path query searchQuery = + let + searchStrings = + List.map (\( name, SearchQuery val ) -> name ++ "=" ++ val) searchQuery + |> String.join "&" + in + Url.Builder.absolute path query + |> (\str -> + str + ++ (case query of + [] -> + "?" ++ searchStrings + + _ -> + "&" ++ searchStrings + ) + ) diff --git a/src/Search.elm b/src/Search.elm index 39bbaec..9c8e10a 100644 --- a/src/Search.elm +++ b/src/Search.elm @@ -62,11 +62,23 @@ import Http import Json.Decode import Json.Encode import RemoteData +import Route exposing (Route) import Set import Task +import Url import Url.Builder +type alias SearchRoute = + Maybe String + -> Maybe String + -> Maybe String + -> Maybe Int + -> Maybe Int + -> Maybe String + -> Route + + type alias Model a = { channel : String , query : Maybe String @@ -174,12 +186,12 @@ type Msg a update : - String + SearchRoute -> Browser.Navigation.Key -> Msg a -> Model a -> ( Model a, Cmd (Msg a) ) -update path navKey msg model = +update toRoute navKey msg model = case msg of NoOp -> ( model @@ -193,7 +205,7 @@ update path navKey msg model = in ( { model | sort = sort } , createUrl - path + toRoute model.channel model.query model.show @@ -218,7 +230,7 @@ update path navKey msg model = else createUrl - path + toRoute channel model.query model.show @@ -240,7 +252,7 @@ update path navKey msg model = else ( { model | result = RemoteData.Loading } , createUrl - path + toRoute model.channel model.query model.show @@ -258,7 +270,7 @@ update path navKey msg model = ShowDetails selected -> ( model , createUrl - path + toRoute model.channel model.query (if model.show == Just selected then @@ -275,8 +287,10 @@ update path navKey msg model = ) +{-| TODO: Sort should be part of Route type +-} createUrl : - String + SearchRoute -> String -> Maybe String -> Maybe String @@ -284,30 +298,9 @@ createUrl : -> Int -> Sort -> String -createUrl path channel query show from size sort = - [ Url.Builder.int "from" from - , Url.Builder.int "size" size - , Url.Builder.string "sort" <| toSortId sort - , Url.Builder.string "channel" channel - ] - |> List.append - (query - |> Maybe.map - (\x -> - [ Url.Builder.string "query" x ] - ) - |> Maybe.withDefault [] - ) - |> List.append - (show - |> Maybe.map - (\x -> - [ Url.Builder.string "show" x - ] - ) - |> Maybe.withDefault [] - ) - |> Url.Builder.absolute [ path ] +createUrl toRoute channel query show from size sort = + toRoute (Just channel) query show (Just from) (Just size) (Just <| toSortId sort) + |> Route.routeToString @@ -320,6 +313,7 @@ type Channel | Release_20_03 | Release_20_09 + type alias ChannelDetails = { id : String , title : String @@ -455,13 +449,15 @@ fromSortId id = view : - String + { toRoute : SearchRoute + , categoryName : String + } -> String -> Model a -> (String -> Maybe String -> SearchResult a -> Html b) -> (Msg a -> b) -> Html b -view path title model viewSuccess outMsg = +view { toRoute, categoryName } title model viewSuccess outMsg = div [ class "search-page" ] @@ -520,14 +516,15 @@ view path title model viewSuccess outMsg = [ type_ "text" , id "search-query-input" , autofocus True - , placeholder <| "Search for " ++ path - , onInput (\x -> outMsg (QueryInput x)) + , placeholder <| "Search for " ++ categoryName + , onInput (outMsg << QueryInput) , value <| Maybe.withDefault "" model.query ] [] , div [ class "loader" ] [] , div [ class "btn-group" ] - [ button [ class "btn" ] [ text "Search" ] + [ button [ class "btn", type_ "submit" ] + [ text "Search" ] ] ] ] @@ -542,8 +539,7 @@ view path title model viewSuccess outMsg = RemoteData.Success result -> if result.hits.total.value == 0 then div [] - [ h4 [] [ text <| "No " ++ path ++ " found!" ] - ] + [ h4 [] [ text <| "No " ++ categoryName ++ " found!" ] ] else div [] @@ -594,9 +590,9 @@ view path title model viewSuccess outMsg = ] ] ] - , viewPager outMsg model result path + , viewPager outMsg model result toRoute , viewSuccess model.channel model.show result - , viewPager outMsg model result path + , viewPager outMsg model result toRoute ] RemoteData.Failure error -> @@ -629,9 +625,9 @@ viewPager : (Msg a -> b) -> Model a -> SearchResult a - -> String + -> SearchRoute -> Html b -viewPager _ model result path = +viewPager _ model result toRoute = ul [ class "pager" ] [ li [ classList @@ -645,7 +641,7 @@ viewPager _ model result path = else href <| createUrl - path + toRoute model.channel model.query model.show @@ -667,7 +663,7 @@ viewPager _ model result path = else createUrl - path + toRoute model.channel model.query model.show @@ -689,7 +685,7 @@ viewPager _ model result path = else createUrl - path + toRoute model.channel model.query model.show @@ -719,7 +715,7 @@ viewPager _ model result path = 0 in createUrl - path + toRoute model.channel model.query model.show