diff --git a/elm.json b/elm.json index 80a54a3..0ce87a4 100644 --- a/elm.json +++ b/elm.json @@ -13,8 +13,10 @@ "elm/http": "2.0.0", "elm/json": "1.1.3", "elm/url": "1.0.0", + "elm-community/html-extra": "3.4.0", "elm-explorations/markdown": "1.0.0", "hecrj/html-parser": "2.3.4", + "justinmimbs/date": "4.1.0", "krisajenkins/remotedata": "6.0.1" }, "indirect": { diff --git a/front/elm/Commands.elm b/front/elm/Commands.elm index 2a4a837..db7cf65 100644 --- a/front/elm/Commands.elm +++ b/front/elm/Commands.elm @@ -3,13 +3,15 @@ module Commands exposing (fetchData) import CatGifs.Commands exposing (fetchCatGif) import Messages exposing (Msg(..)) import Projects.Decoders +import Talks.Decoders import TiledList import Url exposing (Url) -fetchData : Url -> Url -> Cmd Msg -fetchData projectsUrl catGifsUrl = +fetchData : Url -> Url -> Url -> Cmd Msg +fetchData projectsUrl talksUrl catGifsUrl = Cmd.batch [ Cmd.map ProjectsMsg (TiledList.fetch Projects.Decoders.decoder projectsUrl) + , Cmd.map TalksMsg (TiledList.fetch Talks.Decoders.decoder talksUrl) , fetchCatGif catGifsUrl ] diff --git a/front/elm/Decoders.elm b/front/elm/Decoders.elm new file mode 100644 index 0000000..553b84b --- /dev/null +++ b/front/elm/Decoders.elm @@ -0,0 +1,24 @@ +module Decoders exposing (decodeDate, decodeUrl) + +import Date exposing (Date) +import Json.Decode as Decode exposing (Decoder) +import Url exposing (Protocol(..), Url) + + +decodeDate : Decoder Date +decodeDate = + Decode.andThen + (\s -> + case Date.fromIsoString s of + Ok l -> + Decode.succeed l + + Err e -> + Decode.fail e + ) + Decode.string + + +decodeUrl : Decoder (Maybe Url) +decodeUrl = + Decode.map Url.fromString Decode.string diff --git a/front/elm/Lang.elm b/front/elm/Lang.elm new file mode 100644 index 0000000..65dd1a1 --- /dev/null +++ b/front/elm/Lang.elm @@ -0,0 +1,45 @@ +module Lang exposing (Lang(..), decoder, toString) + +import Json.Decode as Decode exposing (Decoder) + + +type Lang + = EN + | FR + + +decoder : Decoder Lang +decoder = + Decode.andThen + (\s -> + case fromString s of + Ok l -> + Decode.succeed l + + Err e -> + Decode.fail e + ) + Decode.string + + +fromString : String -> Result String Lang +fromString s = + case s of + "FR" -> + Ok FR + + "EN" -> + Ok EN + + _ -> + Err "invalid language" + + +toString : Lang -> String +toString l = + case l of + FR -> + "FR" + + EN -> + "EN" diff --git a/front/elm/Link.elm b/front/elm/Link.elm index 22780ac..bbacd7f 100644 --- a/front/elm/Link.elm +++ b/front/elm/Link.elm @@ -1,4 +1,4 @@ -module Link exposing (Model, Msg(..), decoder, view) +module Link exposing (Model, Msg(..), decoder, decoderWithDefaultValue, view) {-| This represent a link to an external resource used as a reference. -} @@ -26,6 +26,11 @@ decoder = |> optional "value" (Decode.map Just Decode.string) Nothing +decoderWithDefaultValue : String -> Decode.Decoder Model +decoderWithDefaultValue defaultValue = + Decode.map (\slides -> { slides | value = Just defaultValue }) decoder + + view : Model -> Html Msg view link = let diff --git a/front/elm/Main.elm b/front/elm/Main.elm index e49d8b6..4297bff 100644 --- a/front/elm/Main.elm +++ b/front/elm/Main.elm @@ -9,6 +9,7 @@ import Json.Decode exposing (decodeString) import Messages exposing (Msg(..)) import Models exposing (Flags, Model, initialModel) import Projects.Models exposing (projectsDefaultUrl) +import Talks.Models exposing (talksDefaultUrl) import Update exposing (update) import Url exposing (Protocol(..), Url) import View exposing (view) @@ -29,9 +30,12 @@ initialState flags url key = projectsUrl = flags.projectsUrl |> toUrl projectsDefaultUrl + + talksUrl = + flags.talksUrl |> toUrl talksDefaultUrl in ( initialModel key url catGifUrl - , fetchData projectsUrl catGifUrl + , fetchData projectsUrl talksUrl catGifUrl ) diff --git a/front/elm/Messages.elm b/front/elm/Messages.elm index 1b2b103..12f7d5a 100644 --- a/front/elm/Messages.elm +++ b/front/elm/Messages.elm @@ -4,6 +4,7 @@ import Browser exposing (UrlRequest) import CatGifs.Models exposing (CatGif) import Projects.Models exposing (Project) import RemoteData exposing (WebData) +import Talks.Models exposing (Talk) import TiledList import Url exposing (Url) @@ -15,3 +16,4 @@ type Msg | NavigateTo String | RedirectTo String | ProjectsMsg (TiledList.Msg Project) + | TalksMsg (TiledList.Msg Talk) diff --git a/front/elm/Models.elm b/front/elm/Models.elm index 92f0c93..f8a11b1 100644 --- a/front/elm/Models.elm +++ b/front/elm/Models.elm @@ -6,6 +6,7 @@ import Projects.Models exposing (Project) import RemoteData exposing (WebData) import Routing exposing (Route) import SocialMedia.Models exposing (SocialMedium, initialSocialMedia) +import Talks.Models exposing (Talk) import TiledList import Url exposing (Url) import Url.Parser exposing (parse) @@ -18,11 +19,13 @@ type alias Model = , key : Key , route : Maybe Route , projects : TiledList.Model Project + , talks : TiledList.Model Talk } type alias Flags = { projectsUrl : String + , talksUrl : String , catGifsUrl : String } @@ -35,4 +38,5 @@ initialModel key url catGifsUrl = , key = key , route = Url.Parser.parse Routing.routeParser url , projects = TiledList.initialModel + , talks = TiledList.initialModel } diff --git a/front/elm/Routing.elm b/front/elm/Routing.elm index c26d75d..970c084 100644 --- a/front/elm/Routing.elm +++ b/front/elm/Routing.elm @@ -1,4 +1,4 @@ -module Routing exposing (Route(..), blogPath, cvPath, meowPath, projectsPath, rootPath, routeParser) +module Routing exposing (Route(..), blogPath, cvPath, meowPath, projectsPath, rootPath, routeParser, talksPath) import Url.Parser exposing (Parser, map, oneOf, s, top) @@ -8,6 +8,7 @@ type Route | CVRoute | MeowRoute | ProjectsRoute + | TalksRoute routeParser : Parser (Route -> a) a @@ -17,6 +18,7 @@ routeParser = , map CVRoute (s cvPath) , map MeowRoute (s meowPath) , map ProjectsRoute (s projectsPath) + , map TalksRoute (s talksPath) ] @@ -43,3 +45,8 @@ projectsPath = rootPath : String rootPath = "/" + + +talksPath : String +talksPath = + "talks" diff --git a/front/elm/Talks/Decoders.elm b/front/elm/Talks/Decoders.elm new file mode 100644 index 0000000..a264b66 --- /dev/null +++ b/front/elm/Talks/Decoders.elm @@ -0,0 +1,34 @@ +module Talks.Decoders exposing (decoder) + +import Decoders exposing (decodeDate) +import Json.Decode as Decode +import Json.Decode.Pipeline exposing (optional, required) +import Lang +import Link +import Talks.Models exposing (Conference, Talk) + + +decoder : Decode.Decoder (List Talk) +decoder = + Decode.list talkDecoder + + +talkDecoder : Decode.Decoder Talk +talkDecoder = + Decode.succeed Talk + |> required "id" Decode.int + |> required "title" Decode.string + |> required "description" Decode.string + |> required "conferences" (Decode.list conferenceDecoder) + + +conferenceDecoder : Decode.Decoder Conference +conferenceDecoder = + Decode.succeed Conference + |> required "organisation" Decode.string + |> required "date" decodeDate + |> required "duration" Decode.int + |> required "lang" Lang.decoder + |> optional "recording" (Decode.map Just (Link.decoderWithDefaultValue "📺 recording")) Nothing + |> optional "slides" (Decode.map Just (Link.decoderWithDefaultValue "📜 slides")) Nothing + |> optional "sources" (Decode.map Just (Link.decoderWithDefaultValue "🧑\u{200D}💻 sources")) Nothing diff --git a/front/elm/Talks/Models.elm b/front/elm/Talks/Models.elm new file mode 100644 index 0000000..424d889 --- /dev/null +++ b/front/elm/Talks/Models.elm @@ -0,0 +1,34 @@ +module Talks.Models exposing (Conference, Talk, talksDefaultUrl) + +import Date exposing (Date) +import Lang exposing (Lang) +import Link +import Url exposing (Protocol(..), Url) + + +type alias Id = + Int + + +type alias Conference = + { organisation : String + , date : Date + , duration : Int + , lang : Lang + , recording : Maybe Link.Model + , slides : Maybe Link.Model + , sources : Maybe Link.Model + } + + +type alias Talk = + { id : Id + , title : String + , description : String + , conferences : List Conference + } + + +talksDefaultUrl : Url +talksDefaultUrl = + Url Https "www.xaviermaso.com" Nothing "api/talks" Nothing Nothing diff --git a/front/elm/Talks/Views.elm b/front/elm/Talks/Views.elm new file mode 100644 index 0000000..3e96bc0 --- /dev/null +++ b/front/elm/Talks/Views.elm @@ -0,0 +1,108 @@ +module Talks.Views exposing (renderCurrent) + +import Colours exposing (Colour, toStringLight) +import Date exposing (format, year) +import Html exposing (Html, br, div, h1, h3, i, li, span, text, ul) +import Html.Attributes exposing (class) +import Html.Events exposing (onClick) +import Html.Extra exposing (viewMaybe) +import Lang +import Link +import Talks.Models exposing (Conference, Talk) +import TiledList exposing (Msg(..)) + + +renderCurrent : Colour -> Talk -> Html (Msg Talk) +renderCurrent colour talk = + let + allYears = + talk.conferences |> List.map (\c -> c.date |> year) + + yearMin = + allYears |> List.minimum + + yearMax = + allYears |> List.maximum + + dates = + case ( yearMin, yearMax ) of + ( Just min, Just max ) -> + Just + (case min == max of + True -> + String.fromInt max + + False -> + String.fromInt min ++ "-" ++ String.fromInt max + ) + + _ -> + Nothing + + foo = + dates |> viewMaybe (\ds -> h3 [ class "date" ] [ text ds ]) + in + div [ class "row" ] + [ div [ class "col-md-12" ] + [ div [ class ("list-component-description " ++ toStringLight colour) ] + [ h1 [] [ text talk.title ] + , foo + + -- , h3 + -- [ class "date" ] + -- [ text dates ] + , div [ class "row" ] + [ div + [ class "col-md-12 textDesc" ] + [ text talk.description ] + ] + , renderConferences talk.conferences + , i + [ class "fa fa-close fa-2x close" + , onClick (CloseDescriptionOf talk) + ] + [] + ] + ] + ] + + +renderConferences : List Conference -> Html (Msg Talk) +renderConferences conferences = + if List.isEmpty conferences then + div [] [] + + else + div [] + [ br [] [] + , div [ class "textDesc" ] [ text "Presented at the following venues:" ] + , conferences |> List.map renderConference |> ul [ class "list-group list-group-flush" ] + ] + + +renderConference : Conference -> Html (Msg Talk) +renderConference c = + let + date = + c.date |> format "MMMM y" + + recording = + c.recording |> viewMaybe (Link.view >> Html.map LinkMsg >> (\s -> div [] [ s, span [ class "textDesc" ] [ text (" (" ++ (c.duration |> String.fromInt) ++ "min)") ] ])) + + slides = + c.slides |> viewMaybe (Link.view >> Html.map LinkMsg) + + sources = + c.sources |> viewMaybe (Link.view >> Html.map LinkMsg) + in + li [ class "list-group-item" ] + [ div [ class "me-auto" ] + [ div [ class "row" ] + [ div [ class "col-md-2 textDesc" ] [ text date ] + , div [ class "col-md-3 textDesc" ] [ text ("[" ++ (c.lang |> Lang.toString) ++ "] " ++ c.organisation) ] + , div [ class "col-md-2" ] [ sources ] + , div [ class "col-md-2" ] [ slides ] + , div [ class "col-md-3" ] [ recording ] + ] + ] + ] diff --git a/front/elm/Update.elm b/front/elm/Update.elm index 21a3d7b..7bcf4ab 100644 --- a/front/elm/Update.elm +++ b/front/elm/Update.elm @@ -46,6 +46,7 @@ update msg model = voidedModel = { model | projects = voidCurrent model.projects + , talks = voidCurrent model.talks } in ( { voidedModel | route = route } @@ -70,6 +71,12 @@ update msg model = ((\m ps -> { m | projects = ps }) model) (Cmd.map ProjectsMsg) + TalksMsg tMsg -> + TiledList.update tMsg model.talks + |> mapBoth + ((\m ts -> { m | talks = ts }) model) + (Cmd.map TalksMsg) + RedirectTo path -> let command = diff --git a/front/elm/View.elm b/front/elm/View.elm index 9e9fcec..61d5832 100644 --- a/front/elm/View.elm +++ b/front/elm/View.elm @@ -9,8 +9,9 @@ import Html.Events exposing (onClick) import Messages exposing (Msg(..)) import Models exposing (Model) import Projects.Views -import Routing exposing (blogPath, cvPath, meowPath, projectsPath, rootPath) +import Routing exposing (blogPath, cvPath, meowPath, projectsPath, rootPath, talksPath) import SocialMedia.View exposing (view) +import Talks.Views import TiledList @@ -43,6 +44,11 @@ view model = , body = [ layoutify (projectsView model) ] } + Routing.TalksRoute -> + { title = "XM | Talks" + , body = [ layoutify (talksView model) ] + } + Nothing -> { title = "XM | 404" , body = [ layoutify notFoundView ] @@ -78,6 +84,21 @@ meowView model = ] +talksView : Model -> Html Msg +talksView model = + div [] + [ div [ class "list-component-header" ] + [ text "As a firm believer in education for everyone, at all times, I sometimes perform presentations where I\u{00A0}share knowledge and communicate feedback about things I explored." ] + , Html.map TalksMsg + (TiledList.view + model.talks + 2 + Talks.Views.renderCurrent + Magenta + ) + ] + + cvView : Html Msg cvView = div [ class "row" ] @@ -106,13 +127,14 @@ mainView = div [ class "row" ] (List.map (\( colour, action, text_ ) -> - div [ class "col-md-4" ] + div [ class "col-md-3" ] [ button [ class ("tile " ++ Colours.toString colour), onClick action ] [ text text_ ] ] ) [ ( Blue, RedirectTo blogPath, "Blog" ) , ( Green, NavigateTo projectsPath, "Projects" ) , ( Orange, NavigateTo cvPath, "CV" ) + , ( Magenta, NavigateTo talksPath, "Talks" ) ] ) diff --git a/front/static/index.js b/front/static/index.js index 70ebffa..d96266c 100644 --- a/front/static/index.js +++ b/front/static/index.js @@ -8,12 +8,14 @@ var API = (process.env.NODE_ENV === 'production') ? prodAPI : devAPI var catGifsUrl = API + '/meow' var projectsUrl = API + '/projects' +var talksUrl = API + '/talks' var Elm = require('../elm/Main').Elm Elm.Main.init({ flags: { projectsUrl: projectsUrl, + talksUrl: talksUrl, catGifsUrl: catGifsUrl } }) diff --git a/front/static/styles/main.scss b/front/static/styles/main.scss index 49ed39c..ccb16e8 100644 --- a/front/static/styles/main.scss +++ b/front/static/styles/main.scss @@ -7,8 +7,10 @@ @import '~bootstrap/scss/utilities'; @import '~bootstrap/scss/reboot'; +@import '~bootstrap/scss/root'; @import '~bootstrap/scss/containers'; @import '~bootstrap/scss/grid'; +@import "~bootstrap/scss/list-group"; @import '~bootstrap/scss/utilities/api'; @@ -131,3 +133,7 @@ a:hover { font-weight : 500; line-height : 1.2; } + +.list-group { + --bs-list-group-bg: inherit; +}