Add dark mode

This commit is contained in:
Correl Roush 2021-01-23 00:03:27 -05:00
parent 129f249ca2
commit ad9bd2db95
6 changed files with 252 additions and 97 deletions

View file

@ -24,13 +24,30 @@ var room_id = uuid4()
var socket = new Socket("/socket", {params: {player_id: player_id}}) var socket = new Socket("/socket", {params: {player_id: player_id}})
socket.connect() socket.connect()
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
var app = Elm.Main.init({ var app = Elm.Main.init({
node: document.getElementById("elm-main"), node: document.getElementById("elm-main"),
flags: { flags: {
player: player_id, player: player_id,
room: room_id, room: room_id,
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight height: window.innerHeight,
theme: getCookie("theme")
} }
}) })
@ -51,6 +68,10 @@ app.ports.joinRoom.subscribe(options => {
channel.push(action.type, action.data) channel.push(action.type, action.data)
}) })
// Theme changes
app.ports.saveTheme.subscribe(theme => {
document.cookie = "theme=" + theme
})
channel.join() channel.join()
.receive("ok", resp => { .receive("ok", resp => {
console.log("Joined successfully", resp); console.log("Joined successfully", resp);

View file

@ -7,6 +7,7 @@ import Html
import PlanningPokerEntry as Entry import PlanningPokerEntry as Entry
import PlanningPokerNotFound as NotFound import PlanningPokerNotFound as NotFound
import PlanningPokerRoom as Room import PlanningPokerRoom as Room
import PlanningPokerUI as UI
import Url exposing (Url) import Url exposing (Url)
import Url.Parser as Parser exposing ((</>), Parser, s, string) import Url.Parser as Parser exposing ((</>), Parser, s, string)
@ -16,6 +17,7 @@ type alias Flags =
, room : String , room : String
, height : Int , height : Int
, width : Int , width : Int
, theme : String
} }
@ -24,6 +26,7 @@ type alias Model =
, key : Nav.Key , key : Nav.Key
, player : String , player : String
, room : String , room : String
, theme : UI.Theme
, dimensions : { width : Int, height : Int } , dimensions : { width : Int, height : Int }
} }
@ -48,12 +51,25 @@ type Msg
init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg )
init { player, room, width, height } url key = init { player, room, width, height, theme } url key =
let
parseTheme themeString =
case String.toLower themeString of
"light" ->
UI.Light
"dark" ->
UI.Dark
_ ->
UI.Light
in
updateUrl url updateUrl url
{ page = NotFound { page = NotFound
, key = key , key = key
, player = player , player = player
, room = room , room = room
, theme = parseTheme theme
, dimensions = { width = width, height = height } , dimensions = { width = width, height = height }
} }
@ -104,14 +120,15 @@ updateUrl : Url -> Model -> ( Model, Cmd Msg )
updateUrl url model = updateUrl url model =
case Parser.parse parser url of case Parser.parse parser url of
Just Entry -> Just Entry ->
toEntry model (Entry.init model.room) toEntry model (Entry.init model.theme model.room)
Just (Room id) -> Just (Room id) ->
case model.page of case model.page of
EntryPage entryModel -> EntryPage entryModel ->
toRoom model toRoom model
(Room.init (Room.init
{ id = id { theme = entryModel.theme
, id = id
, player = model.player , player = model.player
, roomName = "Planning Poker" , roomName = "Planning Poker"
, playerName = entryModel.playerName , playerName = entryModel.playerName
@ -121,7 +138,8 @@ updateUrl url model =
_ -> _ ->
toRoom model toRoom model
(Room.init (Room.init
{ id = id { theme = model.theme
, id = id
, player = model.player , player = model.player
, roomName = "Planning Poker" , roomName = "Planning Poker"
, playerName = "" , playerName = ""

View file

@ -8,11 +8,13 @@ port module PlanningPokerAPI exposing
, newProfile , newProfile
, reset , reset
, reveal , reveal
, updateTheme
, vote , vote
) )
import Json.Decode as Decode import Json.Decode as Decode
import Json.Encode as Encode import Json.Encode as Encode
import PlanningPokerUI as UI
type RoomAction type RoomAction
@ -28,6 +30,9 @@ port joinRoom : { room : String } -> Cmd msg
port roomActions : Encode.Value -> Cmd msg port roomActions : Encode.Value -> Cmd msg
port saveTheme : String -> Cmd msg
newProfile : { playerName : String } -> Cmd msg newProfile : { playerName : String } -> Cmd msg
newProfile = newProfile =
NewProfile >> encodeAction >> roomActions NewProfile >> encodeAction >> roomActions
@ -74,6 +79,16 @@ encodeAction action =
wrap "reveal" (Encode.object []) wrap "reveal" (Encode.object [])
updateTheme : UI.Theme -> Cmd msg
updateTheme theme =
case theme of
UI.Light ->
saveTheme "light"
UI.Dark ->
saveTheme "dark"
port gotPresenceState : (Decode.Value -> msg) -> Sub msg port gotPresenceState : (Decode.Value -> msg) -> Sub msg

View file

@ -13,7 +13,8 @@ import PlanningPokerUI as UI
type alias Model = type alias Model =
{ room : String { theme : UI.Theme
, room : String
, playerName : String , playerName : String
, player : Maybe String , player : Maybe String
, error : Maybe String , error : Maybe String
@ -22,12 +23,14 @@ type alias Model =
type Msg type Msg
= PlayerNameChanged String = PlayerNameChanged String
| UpdateTheme UI.Theme
| CreateRoom | CreateRoom
init : String -> ( Model, Cmd Msg ) init : UI.Theme -> String -> ( Model, Cmd Msg )
init room = init theme room =
( { room = room ( { theme = theme
, room = room
, playerName = "" , playerName = ""
, player = Nothing , player = Nothing
, error = Nothing , error = Nothing
@ -49,40 +52,48 @@ update key msg model =
else else
( model, Cmd.none ) ( model, Cmd.none )
UpdateTheme theme ->
( { model | theme = theme }, API.updateTheme theme )
view : Model -> Document Msg view : Model -> Document Msg
view model = view model =
{ title = "Planning Poker" UI.toDocument model.theme
, body = [ layout model ] { title = "Planning Poker"
} , body = [ layout model ]
}
layout : Model -> Html Msg layout : Model -> Element Msg
layout model = layout model =
Element.layout [] <| column [ width fill, height fill, centerY ]
column [ column
[ width fill, centerY, spacing 30 ] [ width fill, centerY, spacing 30 ]
[ el [ centerX ] (text "Oh, hey!") [ el [ centerX ] (text "Oh, hey!")
, el [ centerX ] (text "Tell us who you are") , el [ centerX ] (text "Tell us who you are")
, Input.text [ centerX, width (px 300), UI.onEnter CreateRoom ] , UI.textInput model.theme
[ centerX, width (px 300), UI.onEnter CreateRoom ]
{ onChange = PlayerNameChanged { onChange = PlayerNameChanged
, text = model.playerName , text = model.playerName
, label = Input.labelHidden "Your name" , label = Input.labelHidden "Your name"
, placeholder = Just (Input.placeholder [] (text "Your name")) , placeholder = Just (Input.placeholder [] (text "Your name"))
} }
, el [ centerX ] (text "then") , el [ centerX ] (text "then")
, UI.actionButton [ centerX ] , UI.actionButton model.theme
[ centerX ]
{ isActive = not (String.isEmpty model.playerName) { isActive = not (String.isEmpty model.playerName)
, onPress = CreateRoom , onPress = CreateRoom
, label = text "Make a room!" , label = text "Make a room!"
} }
, el , el
[ centerX [ centerX
, Background.color UI.colors.errorBackground , Background.color (UI.colors model.theme).errorBackground
, padding 20 , padding 20
, Font.color UI.colors.errorForeground , Font.color (UI.colors model.theme).errorForeground
, transparent (model.error == Nothing) , transparent (model.error == Nothing)
] ]
<| <|
text (Maybe.withDefault " " model.error) text (Maybe.withDefault " " model.error)
] ]
, column [] [ UI.themePicker model.theme UpdateTheme ]
]

View file

@ -22,7 +22,8 @@ import PlanningPokerUI as UI
type alias Model = type alias Model =
{ state : State { theme : UI.Theme
, state : State
, room : Room , room : Room
, player : String , player : String
, playerName : String , playerName : String
@ -45,6 +46,7 @@ type Msg
| GotVote (Result Decode.Error ReceivedVote) | GotVote (Result Decode.Error ReceivedVote)
| GotReveal | GotReveal
| GotReset | GotReset
| UpdateTheme UI.Theme
type Presence type Presence
@ -89,13 +91,14 @@ type Vote
init : init :
{ id : String { theme : UI.Theme
, id : String
, player : String , player : String
, roomName : String , roomName : String
, playerName : String , playerName : String
} }
-> ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
init { id, player, roomName, playerName } = init { theme, id, player, roomName, playerName } =
let let
room = room =
{ id = id { id = id
@ -115,7 +118,8 @@ init { id, player, roomName, playerName } =
] ]
) )
in in
( { room = room ( { theme = theme
, room = room
, state = state , state = state
, player = player , player = player
, playerName = playerName , playerName = playerName
@ -217,6 +221,9 @@ update key msg model =
, Cmd.none , Cmd.none
) )
UpdateTheme theme ->
( { model | theme = theme }, API.updateTheme theme )
view : { height : Int, width : Int } -> Model -> Document Msg view : { height : Int, width : Int } -> Model -> Document Msg
view dimensions model = view dimensions model =
@ -231,20 +238,22 @@ view dimensions model =
in in
case model.state of case model.state of
Playing -> Playing ->
UI.toDocument UI.toDocument model.theme
{ title = model.room.name { title = model.room.name
, body = , body =
[ navBar { title = model.room.name, playerName = playerName } [ navBar model.theme { title = model.room.name, playerName = playerName }
, viewRoom device model , viewRoom device model
, UI.themePicker model.theme UpdateTheme
] ]
} }
Joining -> Joining ->
UI.toDocument UI.toDocument model.theme
{ title = model.room.name { title = model.room.name
, body = , body =
[ navBar { title = model.room.name, playerName = playerName } [ navBar model.theme { title = model.room.name, playerName = playerName }
, joinForm model.room model.playerName , joinForm model.theme model.room model.playerName
, UI.themePicker model.theme UpdateTheme
] ]
} }
@ -258,7 +267,7 @@ viewRoom device model =
in in
case device.class of case device.class of
Phone -> Phone ->
column [ width fill, spacing 20 ] column [ width fill, spacing 20, centerY ]
[ viewPlayers (Dict.values model.room.players) model.showVotes [ viewPlayers (Dict.values model.room.players) model.showVotes
, el [ width (fillPortion 3), alignTop ] <| , el [ width (fillPortion 3), alignTop ] <|
viewCards model myVote viewCards model myVote
@ -266,7 +275,7 @@ viewRoom device model =
] ]
_ -> _ ->
column [ width fill, spacing 20 ] column [ width fill, spacing 20, centerY, alignTop ]
[ row [ row
[ width fill ] [ width fill ]
[ el [ width (fillPortion 3), alignTop ] <| [ el [ width (fillPortion 3), alignTop ] <|
@ -279,23 +288,16 @@ viewRoom device model =
] ]
navBar : { title : String, playerName : String } -> Element Msg navBar : UI.Theme -> { title : String, playerName : String } -> Element Msg
navBar { title, playerName } = navBar theme { title, playerName } =
row UI.navBar theme
[ Background.color UI.colors.primary
, height (px 50)
, width fill
, padding 10
]
[ el [ el
[ Font.alignLeft [ Font.alignLeft
, Font.color UI.colors.background
, width fill , width fill
] ]
(text title) (text title)
, el , el
[ Font.alignRight [ Font.alignRight
, Font.color UI.colors.background
] ]
(text playerName) (text playerName)
] ]
@ -309,10 +311,10 @@ viewCards model selected =
selectedColor = selectedColor =
if enabled then if enabled then
UI.colors.selected (UI.colors model.theme).selected
else else
UI.colors.disabled (UI.colors model.theme).disabled
card value = card value =
Input.button Input.button
@ -328,7 +330,7 @@ viewCards model selected =
selectedColor selectedColor
else else
UI.colors.background (UI.colors model.theme).background
, Font.size 50 , Font.size 50
] ]
{ onPress = { onPress =
@ -405,13 +407,13 @@ viewPlayers playerList showVotes =
moderatorTools : Model -> Element Msg moderatorTools : Model -> Element Msg
moderatorTools model = moderatorTools model =
row [ centerX, spacing 20 ] row [ centerX, spacing 20 ]
[ UI.actionButton [ UI.actionButton model.theme
[ centerX ] [ centerX ]
{ isActive = not model.showVotes { isActive = not model.showVotes
, onPress = Reveal , onPress = Reveal
, label = text "Reveal" , label = text "Reveal"
} }
, UI.actionButton , UI.actionButton model.theme
[ centerX ] [ centerX ]
{ isActive = True { isActive = True
, onPress = Reset , onPress = Reset
@ -420,8 +422,8 @@ moderatorTools model =
] ]
joinForm : Room -> String -> Element Msg joinForm : UI.Theme -> Room -> String -> Element Msg
joinForm room playerName = joinForm theme room playerName =
let let
players = players =
Dict.values room.players Dict.values room.players
@ -441,7 +443,8 @@ joinForm room playerName =
, label = Input.labelHidden "Your name" , label = Input.labelHidden "Your name"
, placeholder = Just (Input.placeholder [] (text "Your name")) , placeholder = Just (Input.placeholder [] (text "Your name"))
} }
, UI.actionButton [ centerX ] , UI.actionButton theme
[ centerX ]
{ isActive = not (String.isEmpty playerName) { isActive = not (String.isEmpty playerName)
, onPress = JoinRoom , onPress = JoinRoom
, label = text "Join!" , label = text "Join!"

View file

@ -1,9 +1,13 @@
module PlanningPokerUI exposing module PlanningPokerUI exposing
( actionButton ( Theme(..)
, actionButton
, colors , colors
, fontSizes , fontSizes
, heroText , heroText
, navBar
, onEnter , onEnter
, textInput
, themePicker
, toDocument , toDocument
) )
@ -16,20 +20,63 @@ import Html.Events
import Json.Decode as Decode import Json.Decode as Decode
colors = type Theme
= Light
| Dark
colors theme =
let let
primary = blue : Color
blue blue =
rgb255 100 100 255
lightGrey : Color
lightGrey =
rgb255 200 200 200
grey : Color
grey =
rgb255 50 50 50
darkGrey : Color
darkGrey =
rgb255 20 20 20
red : Color
red =
rgb255 255 100 100
white : Color
white =
rgb255 255 255 255
black : Color
black =
rgb255 0 0 0
in in
{ primary = primary case theme of
, background = white Light ->
, selected = primary { primary = blue
, disabled = lightGrey , background = white
, errorBackground = red , selected = blue
, errorForeground = white , disabled = lightGrey
, text = black , errorBackground = red
, buttonText = white , errorForeground = white
} , text = black
, buttonText = white
}
Dark ->
{ primary = blue
, background = darkGrey
, selected = blue
, disabled = grey
, errorBackground = red
, errorForeground = white
, text = white
, buttonText = lightGrey
}
fontSizes = fontSizes =
@ -39,56 +86,58 @@ fontSizes =
} }
blue : Color textInput :
blue = Theme
rgb255 100 100 255 -> List (Attribute msg)
->
{ onChange : String -> msg
, text : String
, label : Input.Label msg
, placeholder : Maybe (Input.Placeholder msg)
}
-> Element msg
textInput theme attrs { onChange, text, label, placeholder } =
Input.text
(Font.color (colors theme).text
:: Background.color (colors theme).background
:: attrs
)
{ onChange = onChange, text = text, label = label, placeholder = placeholder }
lightGrey : Color button :
lightGrey = Theme
rgb255 200 200 200 -> List (Attribute msg)
red : Color
red =
rgb255 255 100 100
white : Color
white =
rgb255 255 255 255
black : Color
black =
rgb255 0 0 0
actionButton :
List (Attribute msg)
-> { isActive : Bool, onPress : msg, label : Element msg } -> { isActive : Bool, onPress : msg, label : Element msg }
-> Element msg -> Element msg
actionButton attrs { isActive, onPress, label } = button theme attrs { isActive, onPress, label } =
let let
( color, maybeEvent ) = ( color, maybeEvent ) =
if isActive then if isActive then
( blue, Just onPress ) ( (colors theme).primary, Just onPress )
else else
( lightGrey, Nothing ) ( (colors theme).disabled, Nothing )
in in
Input.button Input.button
([ padding 20 (Background.color color
, Background.color color :: Font.color (colors theme).buttonText
, Font.color white :: attrs
]
++ attrs
) )
{ onPress = maybeEvent { onPress = maybeEvent
, label = label , label = label
} }
actionButton :
Theme
-> List (Attribute msg)
-> { isActive : Bool, onPress : msg, label : Element msg }
-> Element msg
actionButton theme attrs opts =
button theme (padding 20 :: attrs) opts
heroText : heroText :
List (Attribute msg) List (Attribute msg)
-> String -> String
@ -97,11 +146,49 @@ heroText attrs s =
el ([ Font.size fontSizes.huge ] ++ attrs) (text s) el ([ Font.size fontSizes.huge ] ++ attrs) (text s)
toDocument : { title : String, body : List (Element msg) } -> Document msg navBar : Theme -> List (Element msg) -> Element msg
toDocument { title, body } = navBar theme elements =
row
[ Background.color (colors theme).primary
, Font.color (colors theme).buttonText
, height (px 50)
, width fill
, padding 10
]
elements
themePicker : Theme -> (Theme -> msg) -> Element msg
themePicker theme onChange =
row []
[ case theme of
Light ->
button theme
[ padding 5 ]
{ isActive = True
, onPress = onChange Dark
, label = text "Dark mode 🌙"
}
Dark ->
button theme
[ padding 5 ]
{ isActive = True
, onPress = onChange Light
, label = text "Light mode 🌞"
}
]
toDocument : Theme -> { title : String, body : List (Element msg) } -> Document msg
toDocument theme { title, body } =
{ title = title { title = title
, body = , body =
[ layout [] <| [ layout
[ Font.color (colors theme).text
, Background.color (colors theme).background
]
<|
column [ width fill, height fill, spacing 20 ] body column [ width fill, height fill, spacing 20 ] body
] ]
} }