module Pages.Top exposing (Model, Msg, Params, page) import Enum exposing (Enum) import Html exposing (Html, a, button, div, h2, h5, i, img, li, p, span, text, ul, form, label, select, option, input) import Html.Attributes exposing (alt, class, href, src, style, id, for, value) import Html.Events exposing (onClick, onInput) import Http import Json.Decode as Decode import Json.Decode.Extra as Decode exposing (andMap) import Maybe exposing (withDefault) import Spa.Document exposing (Document) import Spa.Generated.Route as Route import Spa.Page as Page exposing (Page) import Spa.Url as Url exposing (Url) import String.Extra exposing (ellipsis) import Util exposing (Direction(..), andThen, by) page : Page Params Model Msg page = Page.element { init = init , update = update , view = view , subscriptions = subscriptions } -- INIT type alias Params = () type alias App = { name : String , category : String , repository : Maybe String , versions : Maybe (List String) , icon : Maybe String , status : Int , slug : String , default_branch : String , website : Maybe String , description : Maybe String } type alias Model = { filter_score : Maybe Int , filter_category : Maybe Category , filter_text : Maybe String , status : Status , apps : (List App) , results : (List App) } type Status = Failure | Loading | Success type Category = Apps | Utilities | Development | Graveyard | All categories : Enum Category categories = Enum.create [ ( "Apps", Apps ) , ( "Utilities", Utilities ) , ( "Development", Development ) , ("All", All) ] init : Url Params -> ( Model, Cmd Msg ) init { params } = ( default_model, loadApps ) default_image : String default_image = "/logo.png" default_model : Model default_model = { filter_score = Nothing, filter_category = Nothing, filter_text = Nothing, status = Loading, apps = [], results = [] } -- UPDATE type Msg = MorePlease | FilterScore String | FilterCategory String | FilterText String | GotApps (Result Http.Error (List App)) filterAppsScore : List App -> Maybe Int -> List App filterAppsScore apps score = case score of Just s -> List.filter (\app -> app.status <= s ) apps Nothing -> apps filterAppsCategory : List App -> Maybe Category -> List App filterAppsCategory apps category = case category of Just c -> if c == All then apps else List.filter (\app -> app.category == categories.toString c ) apps Nothing -> apps filterAppsText : List App -> Maybe String -> List App filterAppsText apps text = case text of Just "" -> apps Just t -> List.filter (\app -> String.contains (String.toLower t) ( String.toLower app.name ++ String.toLower ( Maybe.withDefault "" app.description ) ) ) apps Nothing -> apps update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of MorePlease -> ( default_model, loadApps ) FilterScore filter -> let filter_score = String.toInt filter in ( { model | filter_score = filter_score , results = filterAppsScore ( filterAppsCategory ( filterAppsText model.apps model.filter_text ) model.filter_category ) (String.toInt filter) }, Cmd.none ) FilterCategory filter -> let category = categories.fromString filter in ( { model | filter_category = category , results = filterAppsCategory ( filterAppsScore ( filterAppsText model.apps model.filter_text )model.filter_score ) category }, Cmd.none ) FilterText filter -> ( { model | filter_text = Just filter , results = filterAppsText ( filterAppsScore ( filterAppsCategory model.apps model.filter_category ) model.filter_score ) (Just filter) }, Cmd.none ) GotApps result -> case result of Ok apps -> ( { default_model | status = Success , apps = apps , results = apps }, Cmd.none ) Err _ -> ( { default_model | status = Failure}, Cmd.none ) subscriptions : Model -> Sub Msg subscriptions model = Sub.none -- VIEW view : Model -> Document Msg view model = { title = "abra recipes" , body = [ body model ] } body : Model -> Html Msg body model = div [ class "pt-3" ] [ viewApps model ] viewStatusBadge : App -> Html Msg viewStatusBadge app = let status_class = case app.status of 1 -> "badge-success" 2 -> "badge-info" 3 -> "badge-warning" 4 -> "badge-danger" _ -> "badge-dark" in span [ class ("card-link badge " ++ status_class) ] [ text ("Score: " ++ String.fromInt app.status) ] viewApp : App -> Html Msg viewApp app = let icon_url = case app.icon of Just "" -> default_image Just i -> i Nothing -> default_image repository_link = case app.repository of Just link -> a [ class "card-link", href link ] [ i [ class "fab fa-git-alt" ] [] , text "code" ] Nothing -> text "" website_link = case app.website of Just link -> case link of "" -> text "" _ -> a [ class "card-link", href link ] [ i [ class "fas fa-home" ] [] , text "homepage" ] Nothing -> text "" app_href = Route.toString <| Route.App_String { app = app.slug } in div [ class "col-md-4 mb-4 col-sm-12" ] [ div [ class "smaller-card" ] [ img [ class "card-img-top", src icon_url, alt ("icon for " ++ app.name) ] [] , div [ class "card-body" ] [ h5 [ class "title-container" ] [ a [ class "title", href app_href ] [ text app.name ] ] , p [ class "card-description" ] [ text (ellipsis 100 (withDefault "" app.description)) ] ] , div [ class "footer" ] [ span [ class "badge app-category" ] [ text app.category ]] ] ] viewCategories : (String, Category) -> Html Msg viewCategories category = div [ class "category-tile", onClick (FilterCategory (Tuple.first category)) ] [text (Tuple.first category)] viewApps : Model -> Html Msg viewApps model = case model.status of Failure -> div [] [ div [ class "alert alert-danger" ] [ p [] [ text "Unable to load app data" ] , button [ class "btn btn-danger", onClick MorePlease ] [ text "Try Again!" ] ] ] Loading -> div [ class "d-flex align-items-center", style "height" "89vh" ] [ div [ class "spinner-border m-auto text-light" ] [ span [ class "sr-only" ] [ text "Loading..." ] ] ] Success -> div [ class "row justify-content-center" ] [ div [ class "col-md-3" ] [ h2 [ class "app-headings" ] [text "Categories"] , div [] (List.map viewCategories categories.list) ] , div [ class "col-md-6" ] [ div [ class "row" ] [ div [ class "col-sm-12" ] [ div [] [ form [ class "search-bar-container" ] [ label [ for "text" ] [ text "Search" ] , input [ class "search-bar-input", id "text", onInput FilterText ] [] , label [ for "level" ] [ text " and show only items that have at least " ] , select [ class "search-dropdown", id "level", onInput FilterScore ] [ option [ ] [ text "any" ] , option [ value "1" ] [ text "1 (production)" ] , option [ value "2" ] [ text "2 (beta)" ] , option [ value "3" ] [ text "3 (alpha)" ] , option [ value "4" ] [ text "4 (pre-alpha)" ] ] , text "builds " ] ] ] ] , h2 [ class "app-headings" ] [ text "Apps" ] , div [ class "row" ] (List.map viewApp (model.results |> List.sortWith (by .status ASC |> andThen (String.toLower << .name) ASC ) ) ) ] , div [class "col-md-3"] [] ] -- HTTP loadApps : Cmd Msg loadApps = Http.get { url = "https://apps.coopcloud.tech/" , expect = Http.expectJson GotApps appListDecoder } featuresDecoder = Decode.oneOf [ Decode.at [ "status" ] Decode.int , Decode.succeed 5 ] appDecoder : Decode.Decoder App appDecoder = Decode.succeed App |> andMap (Decode.field "name" Decode.string) |> andMap (Decode.field "category" Decode.string) |> andMap (Decode.maybe (Decode.field "repository" Decode.string)) |> andMap (Decode.succeed Nothing) |> andMap (Decode.maybe (Decode.field "icon" Decode.string)) |> andMap (Decode.at [ "features" ] featuresDecoder) |> andMap (Decode.succeed "") |> andMap (Decode.field "default_branch" Decode.string) |> andMap (Decode.maybe (Decode.field "website" Decode.string)) |> andMap (Decode.maybe (Decode.field "description" Decode.string)) appListDecoder : Decode.Decoder (List App) appListDecoder = Decode.keyValuePairs appDecoder |> Decode.map buildApp buildApp : List ( String, App ) -> List App buildApp apps = List.map (\( slug, app ) -> { app | slug = slug }) apps