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

498 lines
14 KiB
Elm
Raw Normal View History

2021-04-17 21:42:28 +00:00
module Pages.Top exposing (Model, Msg, Params, page)
import Enum exposing (Enum)
2023-03-28 05:54:26 +00:00
import Html exposing (Html, a, button, div, form, h2, h3, h5, i, img, input, label, li, option, p, select, span, text, ul)
2024-04-01 23:07:38 +00:00
import Html.Attributes exposing (alt, class, classList, for, href, id, src, style, target, type_, value)
2023-03-28 05:49:55 +00:00
import Html.Attributes.Aria exposing (ariaLabel)
import Html.Events exposing (onClick, onInput)
2021-04-17 21:42:28 +00:00
import Http
import Json.Decode as Decode
import Json.Decode.Extra as Decode exposing (andMap)
2021-05-04 16:27:42 +00:00
import Maybe exposing (withDefault)
2021-04-17 21:42:28 +00:00
import Spa.Document exposing (Document)
2021-05-04 16:27:42 +00:00
import Spa.Generated.Route as Route
2021-04-17 21:42:28 +00:00
import Spa.Page as Page exposing (Page)
import Spa.Url as Url exposing (Url)
import String.Extra exposing (ellipsis)
2021-05-04 16:27:42 +00:00
import Util exposing (Direction(..), andThen, by)
2021-04-17 21:42:28 +00:00
page : Page Params Model Msg
page =
Page.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
2021-05-04 16:27:42 +00:00
2023-03-28 05:54:26 +00:00
2021-04-17 21:42:28 +00:00
-- INIT
type alias Params =
()
2021-05-04 16:27:42 +00:00
2021-04-17 21:42:28 +00:00
type alias App =
{ name : String
, category : String
, repository : Maybe String
, versions : Maybe (List String)
2021-04-18 10:47:50 +00:00
, icon : Maybe String
2021-04-25 13:37:32 +00:00
, status : Int
, slug : String
, default_branch : String
, website : Maybe String
, description : Maybe String
2021-04-17 21:42:28 +00:00
}
type alias Model =
{ filter_score : Maybe Int
, filter_category : Maybe Category
2021-08-18 23:23:54 +00:00
, filter_text : Maybe String
, status : Status
2023-03-28 05:54:26 +00:00
, apps : List App
, results : List App
}
type Status
2021-04-17 21:42:28 +00:00
= Failure
| Loading
| Success
type Category
= Apps
| Utilities
| Development
| Graveyard
| All
categories : Enum Category
categories =
Enum.create
2023-03-28 23:35:57 +00:00
[ ( "(Everything)", All )
, ( "Apps", Apps )
, ( "Utilities", Utilities )
, ( "Development", Development )
]
2021-04-17 21:42:28 +00:00
init : Url Params -> ( Model, Cmd Msg )
init { params } =
( default_model, loadApps )
2021-04-17 21:42:28 +00:00
2021-04-18 10:47:50 +00:00
default_image : String
2021-05-04 16:27:42 +00:00
default_image =
"/logo.png"
2021-04-18 10:47:50 +00:00
default_model : Model
default_model =
2023-03-28 05:54:26 +00:00
{ filter_score = Nothing
, filter_category = Nothing
, filter_text = Nothing
, status = Loading
, apps = []
, results = []
}
2021-04-18 10:47:50 +00:00
2021-04-17 21:42:28 +00:00
-- UPDATE
type Msg
= MorePlease
| FilterScore String
| FilterCategory String
2021-08-18 23:23:54 +00:00
| FilterText String
2021-04-17 21:42:28 +00:00
| GotApps (Result Http.Error (List App))
filterAppsScore : List App -> Maybe Int -> List App
filterAppsScore apps score =
case score of
Just s ->
2023-03-28 05:54:26 +00:00
List.filter
(\app ->
2023-03-26 22:47:47 +00:00
app.status >= s
2023-03-28 05:54:26 +00:00
)
apps
Nothing ->
apps
2023-03-28 05:54:26 +00:00
filterAppsCategory : List App -> Maybe Category -> List App
filterAppsCategory apps category =
case category of
Just c ->
if c == All then
apps
2023-03-28 05:54:26 +00:00
else
2023-03-28 05:54:26 +00:00
List.filter
(\app ->
app.category == categories.toString c
)
apps
Nothing ->
apps
2023-03-28 05:54:26 +00:00
2021-08-18 23:23:54 +00:00
filterAppsText : List App -> Maybe String -> List App
filterAppsText apps text =
case text of
Just "" ->
apps
2023-03-28 05:54:26 +00:00
2021-08-18 23:23:54 +00:00
Just t ->
2023-03-28 05:54:26 +00:00
List.filter
(\app ->
String.contains (String.toLower t)
(String.toLower app.name
++ String.toLower
(Maybe.withDefault "" app.description)
)
2021-08-18 23:23:54 +00:00
)
2023-03-28 05:54:26 +00:00
apps
2021-08-18 23:23:54 +00:00
Nothing ->
apps
2023-03-28 05:54:26 +00:00
2021-04-17 21:42:28 +00:00
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MorePlease ->
( default_model, loadApps )
FilterScore filter ->
let
2023-03-28 05:54:26 +00:00
filter_score =
String.toInt filter
in
2023-03-28 05:54:26 +00:00
( { 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
2023-03-28 05:54:26 +00:00
category =
categories.fromString filter
in
( { model
| filter_category = category
2023-03-28 05:54:26 +00:00
, results =
filterAppsCategory
(filterAppsScore
(filterAppsText model.apps model.filter_text)
model.filter_score
)
category
}
, Cmd.none
)
2021-04-17 21:42:28 +00:00
2021-08-18 23:23:54 +00:00
FilterText filter ->
( { model
| filter_text = Just filter
2023-03-28 05:54:26 +00:00
, results =
filterAppsText
(filterAppsScore
(filterAppsCategory model.apps model.filter_category)
model.filter_score
)
(Just filter)
}
, Cmd.none
)
2021-08-18 23:23:54 +00:00
2021-04-17 21:42:28 +00:00
GotApps result ->
case result of
Ok apps ->
( { default_model
2023-03-28 05:54:26 +00:00
| status = Success
, apps = apps
, results = apps
}
, Cmd.none
)
2021-04-17 21:42:28 +00:00
Err _ ->
( { default_model
2023-03-28 05:54:26 +00:00
| status = Failure
}
, Cmd.none
)
2021-04-17 21:42:28 +00:00
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> Document Msg
view model =
2022-05-04 09:19:28 +00:00
{ title = "Co-op Cloud Recipes"
2021-04-17 21:42:28 +00:00
, body = [ body model ]
}
body : Model -> Html Msg
body model =
2023-03-28 15:00:44 +00:00
div []
2021-04-17 21:42:28 +00:00
[ viewApps model
]
2021-05-04 16:27:42 +00:00
2021-04-19 23:31:11 +00:00
viewStatusBadge : App -> Html Msg
viewStatusBadge app =
2021-04-18 10:47:50 +00:00
let
status_class =
case app.status of
2023-03-26 22:47:47 +00:00
5 ->
2021-04-18 10:47:50 +00:00
"badge-success"
2021-05-04 16:27:42 +00:00
2023-03-26 22:47:47 +00:00
4 ->
2021-04-18 10:47:50 +00:00
"badge-info"
2021-05-04 16:27:42 +00:00
2021-04-25 13:37:32 +00:00
3 ->
2021-04-18 10:47:50 +00:00
"badge-warning"
2021-05-04 16:27:42 +00:00
2023-03-26 22:47:47 +00:00
2 ->
2021-04-18 10:47:50 +00:00
"badge-danger"
2021-05-04 16:27:42 +00:00
2021-04-18 10:47:50 +00:00
_ ->
"badge-dark"
in
2021-05-04 16:27:42 +00:00
span [ class ("card-link badge " ++ status_class) ]
2023-03-28 15:00:44 +00:00
[ text ("Score: " ++ String.fromInt app.status) ]
2021-05-04 16:27:42 +00:00
2021-04-18 10:47:50 +00:00
2021-04-19 23:31:11 +00:00
viewApp : App -> Html Msg
viewApp app =
2021-04-18 10:47:50 +00:00
let
icon_url =
case app.icon of
Just "" ->
default_image
2021-05-04 16:27:42 +00:00
2021-04-18 10:47:50 +00:00
Just i ->
i
2021-05-04 16:27:42 +00:00
2021-04-18 10:47:50 +00:00
Nothing ->
default_image
2021-05-04 16:27:42 +00:00
2021-04-25 13:25:25 +00:00
repository_link =
2021-04-18 10:47:50 +00:00
case app.repository of
2021-04-19 23:31:11 +00:00
Just link ->
2021-04-25 13:25:25 +00:00
a [ class "card-link", href link ]
2021-05-04 16:27:42 +00:00
[ i [ class "fab fa-git-alt" ] []
, text "code"
2021-04-19 23:31:11 +00:00
]
2021-05-04 16:27:42 +00:00
2021-04-18 10:47:50 +00:00
Nothing ->
text ""
2021-05-04 16:27:42 +00:00
2021-04-25 13:25:25 +00:00
website_link =
case app.website of
Just link ->
case link of
"" ->
text ""
2021-05-04 16:27:42 +00:00
_ ->
2021-04-25 13:25:25 +00:00
a [ class "card-link", href link ]
[ i [ class "fas fa-home" ] []
2021-05-04 16:27:42 +00:00
, text "homepage"
]
Nothing ->
text ""
2021-05-04 16:27:42 +00:00
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" ]
2021-05-04 16:27:42 +00:00
[ 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)) ]
2021-05-04 16:27:42 +00:00
]
, div [ class "footer" ]
2023-03-28 05:54:26 +00:00
[ viewStatusBadge app, span [ class "badge app-category" ] [ text app.category ] ]
2021-04-17 21:42:28 +00:00
]
2021-05-04 16:27:42 +00:00
]
2021-04-17 21:42:28 +00:00
2021-04-24 13:36:56 +00:00
2023-03-28 23:35:57 +00:00
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) ]
2021-04-17 21:42:28 +00:00
viewApps : Model -> Html Msg
viewApps model =
case model.status of
2021-04-17 21:42:28 +00:00
Failure ->
2021-04-24 14:12:56 +00:00
div []
[ div [ class "alert alert-danger" ]
[ p [] [ text "Unable to load app data" ]
, button [ class "btn btn-danger", onClick MorePlease ] [ text "Try Again!" ]
]
2021-04-17 21:42:28 +00:00
]
Loading ->
2021-04-25 17:10:41 +00:00
div [ class "d-flex align-items-center", style "height" "89vh" ]
[ div [ class "spinner-border m-auto text-light" ]
2021-05-04 16:27:42 +00:00
[ span [ class "sr-only" ] [ text "Loading..." ]
2021-04-25 17:10:41 +00:00
]
]
2021-05-04 16:27:42 +00:00
Success ->
div [ class "row justify-content-center" ]
2023-03-28 05:54:26 +00:00
[ div [ class "col-md-3", id "filter" ]
2023-03-28 15:00:44 +00:00
[ h2 [ class "app-headings" ] [ text "Finding things" ]
2023-03-28 05:54:26 +00:00
, form []
[ div []
[ h3 [] [ text "Search" ]
, input [ ariaLabel "search", id "text", onInput FilterText ] []
]
2023-03-28 23:35:57 +00:00
, div []
[ h3 [] [ text "Categories" ]
, div [] (List.map (viewCategory model) categories.list)
]
2023-03-28 05:54:26 +00:00
, div []
[ h3 [] [ text "Status" ]
2024-04-01 23:07:38 +00:00
, div []
[ label [] [ text "Minimum score: " ]
, a [ class "help", target "_blank", href "https://docs.coopcloud.tech/abra/recipes/#status-features-score" ] [ text "(help)" ]
]
2023-03-28 05:54:26 +00:00
, select [ class "search-dropdown", id "level", onInput FilterScore ]
[ option [] [ text "any" ]
2023-03-28 15:00:44 +00:00
, 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)" ]
2023-03-28 05:54:26 +00:00
]
]
2023-03-28 05:49:55 +00:00
]
]
, div [ class "col-md-6 offset-md-3" ]
[ div [ class "row" ]
2023-03-28 05:54:26 +00:00
[ div [ class "col-sm-12" ]
[ div []
[]
]
]
2023-03-28 23:35:57 +00:00
, div [ id "intro", class "card" ]
[ div [ class "card-body" ]
2023-03-29 01:11:53 +00:00
[ h2 [ class "app-headings card-title" ] [ text "Co-op Cloud Recipes" ]
2023-03-28 23:35:57 +00:00
, text "You can use these recipes ("
, a
[ href
"https://docs.coopcloud.tech/glossary/#recipe"
]
[ text "What's a recipe?"
]
2023-03-28 15:00:44 +00:00
, text ") with "
2023-03-28 23:35:57 +00:00
, a [ href "https://coopcloud.tech" ]
[ text "Co-op Cloud"
]
2023-03-28 15:00:44 +00:00
, text "."
2023-03-28 23:35:57 +00:00
]
2023-03-28 15:00:44 +00:00
]
, div [ class "row" ]
(List.map viewApp
(model.results
|> List.sortWith
2023-03-26 22:47:47 +00:00
(by .status DESC
|> andThen (String.toLower << .name) ASC
)
)
2021-05-04 16:27:42 +00:00
)
]
2023-03-28 05:54:26 +00:00
, div [ class "col-md-3" ] []
2021-04-17 21:42:28 +00:00
]
2023-03-28 05:54:26 +00:00
2021-04-17 21:42:28 +00:00
-- HTTP
loadApps : Cmd Msg
loadApps =
2022-05-03 17:57:37 +00:00
Http.request
{ url = "https://recipes.coopcloud.tech/recipes.json"
2021-04-17 21:42:28 +00:00
, expect = Http.expectJson GotApps appListDecoder
, headers = [ Http.header "Content-Type" "application/json" ]
, body = Http.emptyBody
, method = "GET"
, timeout = Nothing
, tracker = Nothing
2021-04-17 21:42:28 +00:00
}
2021-05-04 16:27:42 +00:00
2021-04-18 10:47:50 +00:00
featuresDecoder =
2021-05-04 16:27:42 +00:00
Decode.oneOf
2021-04-25 13:37:32 +00:00
[ Decode.at [ "status" ] Decode.int
, Decode.succeed 5
2021-04-18 10:47:50 +00:00
]
2021-04-17 21:42:28 +00:00
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))
2021-04-17 21:42:28 +00:00
2021-04-25 13:25:25 +00:00
2021-04-17 21:42:28 +00:00
appListDecoder : Decode.Decoder (List App)
appListDecoder =
2021-04-25 13:25:25 +00:00
Decode.keyValuePairs appDecoder
|> Decode.map buildApp
2021-05-04 16:27:42 +00:00
buildApp : List ( String, App ) -> List App
2021-04-25 13:25:25 +00:00
buildApp apps =
2021-05-04 16:27:42 +00:00
List.map (\( slug, app ) -> { app | slug = slug }) apps