495 lines
14 KiB
Elm
495 lines
14 KiB
Elm
module Pages.Top exposing (Model, Msg, Params, page)
|
|
|
|
import Enum exposing (Enum)
|
|
import Html exposing (Html, a, button, div, form, h2, h3, h5, i, img, input, label, li, option, p, select, span, text, ul)
|
|
import Html.Attributes exposing (alt, class, classList, for, href, id, src, style, value)
|
|
import Html.Attributes.Aria exposing (ariaLabel)
|
|
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
|
|
[ ( "(Everything)", All )
|
|
, ( "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 ->
|
|
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 = "Co-op Cloud Recipes"
|
|
, body = [ body model ]
|
|
}
|
|
|
|
|
|
body : Model -> Html Msg
|
|
body model =
|
|
div []
|
|
[ viewApps model
|
|
]
|
|
|
|
|
|
viewStatusBadge : App -> Html Msg
|
|
viewStatusBadge app =
|
|
let
|
|
status_class =
|
|
case app.status of
|
|
5 ->
|
|
"badge-success"
|
|
|
|
4 ->
|
|
"badge-info"
|
|
|
|
3 ->
|
|
"badge-warning"
|
|
|
|
2 ->
|
|
"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" ]
|
|
[ viewStatusBadge app, span [ class "badge app-category" ] [ text app.category ] ]
|
|
]
|
|
]
|
|
|
|
|
|
viewCategory : Model -> ( String, Category ) -> Html Msg
|
|
viewCategory model category =
|
|
div
|
|
[ classList
|
|
[ ( "category-tile", True )
|
|
, ( "active", categories.toString (Maybe.withDefault All model.filter_category) == Tuple.first category )
|
|
]
|
|
, 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", id "filter" ]
|
|
[ h2 [ class "app-headings" ] [ text "Finding things" ]
|
|
, form []
|
|
[ div []
|
|
[ h3 [] [ text "Search" ]
|
|
, input [ ariaLabel "search", id "text", onInput FilterText ] []
|
|
]
|
|
, div []
|
|
[ h3 [] [ text "Categories" ]
|
|
, div [] (List.map (viewCategory model) categories.list)
|
|
]
|
|
, div []
|
|
[ h3 [] [ text "Status" ]
|
|
, label [ for "level" ] [ text "Minimum score:" ]
|
|
, select [ class "search-dropdown", id "level", onInput FilterScore ]
|
|
[ option [] [ text "any" ]
|
|
, option [ value "5" ] [ text "5 (amazing)" ]
|
|
, option [ value "4" ] [ text "4 (good)" ]
|
|
, option [ value "3" ] [ text "3 (ok)" ]
|
|
, option [ value "2" ] [ text "2 (basic)" ]
|
|
, option [ value "1" ] [ text "1 (chaos)" ]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
, div [ class "col-md-6 offset-md-3" ]
|
|
[ div [ class "row" ]
|
|
[ div [ class "col-sm-12" ]
|
|
[ div []
|
|
[]
|
|
]
|
|
]
|
|
, div [ id "intro", class "card" ]
|
|
[ div [ class "card-body" ]
|
|
[ h2 [ class "app-headings card-title" ] [ text "Co-op Cloud Recipe Catalogue" ]
|
|
, text "You can use these recipes ("
|
|
, a
|
|
[ href
|
|
"https://docs.coopcloud.tech/glossary/#recipe"
|
|
]
|
|
[ text "What's a recipe?"
|
|
]
|
|
, text ") with "
|
|
, a [ href "https://coopcloud.tech" ]
|
|
[ text "Co-op Cloud"
|
|
]
|
|
, text "."
|
|
]
|
|
]
|
|
, div [ class "row" ]
|
|
(List.map viewApp
|
|
(model.results
|
|
|> List.sortWith
|
|
(by .status DESC
|
|
|> andThen (String.toLower << .name) ASC
|
|
)
|
|
)
|
|
)
|
|
]
|
|
, div [ class "col-md-3" ] []
|
|
]
|
|
|
|
|
|
|
|
-- HTTP
|
|
|
|
|
|
loadApps : Cmd Msg
|
|
loadApps =
|
|
Http.request
|
|
{ url = "https://recipes.coopcloud.tech/recipes.json"
|
|
, expect = Http.expectJson GotApps appListDecoder
|
|
, headers = [ Http.header "Content-Type" "application/json" ]
|
|
, body = Http.emptyBody
|
|
, method = "GET"
|
|
, timeout = Nothing
|
|
, tracker = Nothing
|
|
}
|
|
|
|
|
|
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
|