recipes.coopcloud.tech/src/Pages/Top.elm

409 lines
11 KiB
Elm

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
categories : Enum Category
categories =
Enum.create
[ ( "Apps", Apps )
, ( "Utilities", Utilities )
, ( "Development", Development )
]
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 ->
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-3 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 "card-title" ]
[ a [ href app_href ] [ text app.name ] ]
, p [] [ text (ellipsis 100 (withDefault "" app.description)) ]
]
, div [ class "card-footer" ]
[ span [ class "card-link badge app-category" ] [ text app.category ]]
]
]
viewCategoryOption : (String, Category) -> Html Msg
viewCategoryOption category =
option
[ value (categories.toString (Tuple.second category) ) ]
[ text (categories.toString (Tuple.second 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 []
[ div [ class "row" ]
[ div [ class "col-sm-12" ] [
div [] [
form [ class "card-body form-inline" ] [
label [ for "text" ] [ text "Search" ]
, input [ class "ml-2 form-control", id "text", onInput FilterText ] []
, label [ for "level" ] [ text " and show only items that have at least " ]
, select [ class "ml-2 mr-4 form-control", 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 "
]
]
]
]
, div [ class "row" ]
(List.map viewApp
(model.results
|> List.sortWith
(by .status ASC
|> andThen (String.toLower << .name) ASC
)
)
)
]
-- 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