mirror of
https://github.com/correl/planning-poker.git
synced 2024-11-14 11:09:35 +00:00
Add dark mode
This commit is contained in:
parent
129f249ca2
commit
ad9bd2db95
6 changed files with 252 additions and 97 deletions
|
@ -24,13 +24,30 @@ var room_id = uuid4()
|
|||
var socket = new Socket("/socket", {params: {player_id: player_id}})
|
||||
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({
|
||||
node: document.getElementById("elm-main"),
|
||||
flags: {
|
||||
player: player_id,
|
||||
room: room_id,
|
||||
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)
|
||||
})
|
||||
|
||||
// Theme changes
|
||||
app.ports.saveTheme.subscribe(theme => {
|
||||
document.cookie = "theme=" + theme
|
||||
})
|
||||
channel.join()
|
||||
.receive("ok", resp => {
|
||||
console.log("Joined successfully", resp);
|
||||
|
|
|
@ -7,6 +7,7 @@ import Html
|
|||
import PlanningPokerEntry as Entry
|
||||
import PlanningPokerNotFound as NotFound
|
||||
import PlanningPokerRoom as Room
|
||||
import PlanningPokerUI as UI
|
||||
import Url exposing (Url)
|
||||
import Url.Parser as Parser exposing ((</>), Parser, s, string)
|
||||
|
||||
|
@ -16,6 +17,7 @@ type alias Flags =
|
|||
, room : String
|
||||
, height : Int
|
||||
, width : Int
|
||||
, theme : String
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,6 +26,7 @@ type alias Model =
|
|||
, key : Nav.Key
|
||||
, player : String
|
||||
, room : String
|
||||
, theme : UI.Theme
|
||||
, dimensions : { width : Int, height : Int }
|
||||
}
|
||||
|
||||
|
@ -48,12 +51,25 @@ type 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
|
||||
{ page = NotFound
|
||||
, key = key
|
||||
, player = player
|
||||
, room = room
|
||||
, theme = parseTheme theme
|
||||
, dimensions = { width = width, height = height }
|
||||
}
|
||||
|
||||
|
@ -104,14 +120,15 @@ updateUrl : Url -> Model -> ( Model, Cmd Msg )
|
|||
updateUrl url model =
|
||||
case Parser.parse parser url of
|
||||
Just Entry ->
|
||||
toEntry model (Entry.init model.room)
|
||||
toEntry model (Entry.init model.theme model.room)
|
||||
|
||||
Just (Room id) ->
|
||||
case model.page of
|
||||
EntryPage entryModel ->
|
||||
toRoom model
|
||||
(Room.init
|
||||
{ id = id
|
||||
{ theme = entryModel.theme
|
||||
, id = id
|
||||
, player = model.player
|
||||
, roomName = "Planning Poker"
|
||||
, playerName = entryModel.playerName
|
||||
|
@ -121,7 +138,8 @@ updateUrl url model =
|
|||
_ ->
|
||||
toRoom model
|
||||
(Room.init
|
||||
{ id = id
|
||||
{ theme = model.theme
|
||||
, id = id
|
||||
, player = model.player
|
||||
, roomName = "Planning Poker"
|
||||
, playerName = ""
|
||||
|
|
|
@ -8,11 +8,13 @@ port module PlanningPokerAPI exposing
|
|||
, newProfile
|
||||
, reset
|
||||
, reveal
|
||||
, updateTheme
|
||||
, vote
|
||||
)
|
||||
|
||||
import Json.Decode as Decode
|
||||
import Json.Encode as Encode
|
||||
import PlanningPokerUI as UI
|
||||
|
||||
|
||||
type RoomAction
|
||||
|
@ -28,6 +30,9 @@ port joinRoom : { room : String } -> Cmd msg
|
|||
port roomActions : Encode.Value -> Cmd msg
|
||||
|
||||
|
||||
port saveTheme : String -> Cmd msg
|
||||
|
||||
|
||||
newProfile : { playerName : String } -> Cmd msg
|
||||
newProfile =
|
||||
NewProfile >> encodeAction >> roomActions
|
||||
|
@ -74,6 +79,16 @@ encodeAction action =
|
|||
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
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ import PlanningPokerUI as UI
|
|||
|
||||
|
||||
type alias Model =
|
||||
{ room : String
|
||||
{ theme : UI.Theme
|
||||
, room : String
|
||||
, playerName : String
|
||||
, player : Maybe String
|
||||
, error : Maybe String
|
||||
|
@ -22,12 +23,14 @@ type alias Model =
|
|||
|
||||
type Msg
|
||||
= PlayerNameChanged String
|
||||
| UpdateTheme UI.Theme
|
||||
| CreateRoom
|
||||
|
||||
|
||||
init : String -> ( Model, Cmd Msg )
|
||||
init room =
|
||||
( { room = room
|
||||
init : UI.Theme -> String -> ( Model, Cmd Msg )
|
||||
init theme room =
|
||||
( { theme = theme
|
||||
, room = room
|
||||
, playerName = ""
|
||||
, player = Nothing
|
||||
, error = Nothing
|
||||
|
@ -49,40 +52,48 @@ update key msg model =
|
|||
else
|
||||
( model, Cmd.none )
|
||||
|
||||
UpdateTheme theme ->
|
||||
( { model | theme = theme }, API.updateTheme theme )
|
||||
|
||||
|
||||
view : Model -> Document Msg
|
||||
view model =
|
||||
{ title = "Planning Poker"
|
||||
, body = [ layout model ]
|
||||
}
|
||||
UI.toDocument model.theme
|
||||
{ title = "Planning Poker"
|
||||
, body = [ layout model ]
|
||||
}
|
||||
|
||||
|
||||
layout : Model -> Html Msg
|
||||
layout : Model -> Element Msg
|
||||
layout model =
|
||||
Element.layout [] <|
|
||||
column
|
||||
column [ width fill, height fill, centerY ]
|
||||
[ column
|
||||
[ width fill, centerY, spacing 30 ]
|
||||
[ el [ centerX ] (text "Oh, hey!")
|
||||
, 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
|
||||
, text = model.playerName
|
||||
, label = Input.labelHidden "Your name"
|
||||
, placeholder = Just (Input.placeholder [] (text "Your name"))
|
||||
}
|
||||
, el [ centerX ] (text "then")
|
||||
, UI.actionButton [ centerX ]
|
||||
, UI.actionButton model.theme
|
||||
[ centerX ]
|
||||
{ isActive = not (String.isEmpty model.playerName)
|
||||
, onPress = CreateRoom
|
||||
, label = text "Make a room!"
|
||||
}
|
||||
, el
|
||||
[ centerX
|
||||
, Background.color UI.colors.errorBackground
|
||||
, Background.color (UI.colors model.theme).errorBackground
|
||||
, padding 20
|
||||
, Font.color UI.colors.errorForeground
|
||||
, Font.color (UI.colors model.theme).errorForeground
|
||||
, transparent (model.error == Nothing)
|
||||
]
|
||||
<|
|
||||
text (Maybe.withDefault " " model.error)
|
||||
]
|
||||
, column [] [ UI.themePicker model.theme UpdateTheme ]
|
||||
]
|
||||
|
|
|
@ -22,7 +22,8 @@ import PlanningPokerUI as UI
|
|||
|
||||
|
||||
type alias Model =
|
||||
{ state : State
|
||||
{ theme : UI.Theme
|
||||
, state : State
|
||||
, room : Room
|
||||
, player : String
|
||||
, playerName : String
|
||||
|
@ -45,6 +46,7 @@ type Msg
|
|||
| GotVote (Result Decode.Error ReceivedVote)
|
||||
| GotReveal
|
||||
| GotReset
|
||||
| UpdateTheme UI.Theme
|
||||
|
||||
|
||||
type Presence
|
||||
|
@ -89,13 +91,14 @@ type Vote
|
|||
|
||||
|
||||
init :
|
||||
{ id : String
|
||||
{ theme : UI.Theme
|
||||
, id : String
|
||||
, player : String
|
||||
, roomName : String
|
||||
, playerName : String
|
||||
}
|
||||
-> ( Model, Cmd Msg )
|
||||
init { id, player, roomName, playerName } =
|
||||
init { theme, id, player, roomName, playerName } =
|
||||
let
|
||||
room =
|
||||
{ id = id
|
||||
|
@ -115,7 +118,8 @@ init { id, player, roomName, playerName } =
|
|||
]
|
||||
)
|
||||
in
|
||||
( { room = room
|
||||
( { theme = theme
|
||||
, room = room
|
||||
, state = state
|
||||
, player = player
|
||||
, playerName = playerName
|
||||
|
@ -217,6 +221,9 @@ update key msg model =
|
|||
, Cmd.none
|
||||
)
|
||||
|
||||
UpdateTheme theme ->
|
||||
( { model | theme = theme }, API.updateTheme theme )
|
||||
|
||||
|
||||
view : { height : Int, width : Int } -> Model -> Document Msg
|
||||
view dimensions model =
|
||||
|
@ -231,20 +238,22 @@ view dimensions model =
|
|||
in
|
||||
case model.state of
|
||||
Playing ->
|
||||
UI.toDocument
|
||||
UI.toDocument model.theme
|
||||
{ title = model.room.name
|
||||
, body =
|
||||
[ navBar { title = model.room.name, playerName = playerName }
|
||||
[ navBar model.theme { title = model.room.name, playerName = playerName }
|
||||
, viewRoom device model
|
||||
, UI.themePicker model.theme UpdateTheme
|
||||
]
|
||||
}
|
||||
|
||||
Joining ->
|
||||
UI.toDocument
|
||||
UI.toDocument model.theme
|
||||
{ title = model.room.name
|
||||
, body =
|
||||
[ navBar { title = model.room.name, playerName = playerName }
|
||||
, joinForm model.room model.playerName
|
||||
[ navBar model.theme { title = model.room.name, playerName = playerName }
|
||||
, joinForm model.theme model.room model.playerName
|
||||
, UI.themePicker model.theme UpdateTheme
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -258,7 +267,7 @@ viewRoom device model =
|
|||
in
|
||||
case device.class of
|
||||
Phone ->
|
||||
column [ width fill, spacing 20 ]
|
||||
column [ width fill, spacing 20, centerY ]
|
||||
[ viewPlayers (Dict.values model.room.players) model.showVotes
|
||||
, el [ width (fillPortion 3), alignTop ] <|
|
||||
viewCards model myVote
|
||||
|
@ -266,7 +275,7 @@ viewRoom device model =
|
|||
]
|
||||
|
||||
_ ->
|
||||
column [ width fill, spacing 20 ]
|
||||
column [ width fill, spacing 20, centerY, alignTop ]
|
||||
[ row
|
||||
[ width fill ]
|
||||
[ el [ width (fillPortion 3), alignTop ] <|
|
||||
|
@ -279,23 +288,16 @@ viewRoom device model =
|
|||
]
|
||||
|
||||
|
||||
navBar : { title : String, playerName : String } -> Element Msg
|
||||
navBar { title, playerName } =
|
||||
row
|
||||
[ Background.color UI.colors.primary
|
||||
, height (px 50)
|
||||
, width fill
|
||||
, padding 10
|
||||
]
|
||||
navBar : UI.Theme -> { title : String, playerName : String } -> Element Msg
|
||||
navBar theme { title, playerName } =
|
||||
UI.navBar theme
|
||||
[ el
|
||||
[ Font.alignLeft
|
||||
, Font.color UI.colors.background
|
||||
, width fill
|
||||
]
|
||||
(text title)
|
||||
, el
|
||||
[ Font.alignRight
|
||||
, Font.color UI.colors.background
|
||||
]
|
||||
(text playerName)
|
||||
]
|
||||
|
@ -309,10 +311,10 @@ viewCards model selected =
|
|||
|
||||
selectedColor =
|
||||
if enabled then
|
||||
UI.colors.selected
|
||||
(UI.colors model.theme).selected
|
||||
|
||||
else
|
||||
UI.colors.disabled
|
||||
(UI.colors model.theme).disabled
|
||||
|
||||
card value =
|
||||
Input.button
|
||||
|
@ -328,7 +330,7 @@ viewCards model selected =
|
|||
selectedColor
|
||||
|
||||
else
|
||||
UI.colors.background
|
||||
(UI.colors model.theme).background
|
||||
, Font.size 50
|
||||
]
|
||||
{ onPress =
|
||||
|
@ -405,13 +407,13 @@ viewPlayers playerList showVotes =
|
|||
moderatorTools : Model -> Element Msg
|
||||
moderatorTools model =
|
||||
row [ centerX, spacing 20 ]
|
||||
[ UI.actionButton
|
||||
[ UI.actionButton model.theme
|
||||
[ centerX ]
|
||||
{ isActive = not model.showVotes
|
||||
, onPress = Reveal
|
||||
, label = text "Reveal"
|
||||
}
|
||||
, UI.actionButton
|
||||
, UI.actionButton model.theme
|
||||
[ centerX ]
|
||||
{ isActive = True
|
||||
, onPress = Reset
|
||||
|
@ -420,8 +422,8 @@ moderatorTools model =
|
|||
]
|
||||
|
||||
|
||||
joinForm : Room -> String -> Element Msg
|
||||
joinForm room playerName =
|
||||
joinForm : UI.Theme -> Room -> String -> Element Msg
|
||||
joinForm theme room playerName =
|
||||
let
|
||||
players =
|
||||
Dict.values room.players
|
||||
|
@ -441,7 +443,8 @@ joinForm room playerName =
|
|||
, label = Input.labelHidden "Your name"
|
||||
, placeholder = Just (Input.placeholder [] (text "Your name"))
|
||||
}
|
||||
, UI.actionButton [ centerX ]
|
||||
, UI.actionButton theme
|
||||
[ centerX ]
|
||||
{ isActive = not (String.isEmpty playerName)
|
||||
, onPress = JoinRoom
|
||||
, label = text "Join!"
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
module PlanningPokerUI exposing
|
||||
( actionButton
|
||||
( Theme(..)
|
||||
, actionButton
|
||||
, colors
|
||||
, fontSizes
|
||||
, heroText
|
||||
, navBar
|
||||
, onEnter
|
||||
, textInput
|
||||
, themePicker
|
||||
, toDocument
|
||||
)
|
||||
|
||||
|
@ -16,20 +20,63 @@ import Html.Events
|
|||
import Json.Decode as Decode
|
||||
|
||||
|
||||
colors =
|
||||
type Theme
|
||||
= Light
|
||||
| Dark
|
||||
|
||||
|
||||
colors theme =
|
||||
let
|
||||
primary =
|
||||
blue
|
||||
blue : Color
|
||||
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
|
||||
{ primary = primary
|
||||
, background = white
|
||||
, selected = primary
|
||||
, disabled = lightGrey
|
||||
, errorBackground = red
|
||||
, errorForeground = white
|
||||
, text = black
|
||||
, buttonText = white
|
||||
}
|
||||
case theme of
|
||||
Light ->
|
||||
{ primary = blue
|
||||
, background = white
|
||||
, selected = blue
|
||||
, disabled = lightGrey
|
||||
, errorBackground = red
|
||||
, errorForeground = white
|
||||
, text = black
|
||||
, buttonText = white
|
||||
}
|
||||
|
||||
Dark ->
|
||||
{ primary = blue
|
||||
, background = darkGrey
|
||||
, selected = blue
|
||||
, disabled = grey
|
||||
, errorBackground = red
|
||||
, errorForeground = white
|
||||
, text = white
|
||||
, buttonText = lightGrey
|
||||
}
|
||||
|
||||
|
||||
fontSizes =
|
||||
|
@ -39,56 +86,58 @@ fontSizes =
|
|||
}
|
||||
|
||||
|
||||
blue : Color
|
||||
blue =
|
||||
rgb255 100 100 255
|
||||
textInput :
|
||||
Theme
|
||||
-> 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
|
||||
lightGrey =
|
||||
rgb255 200 200 200
|
||||
|
||||
|
||||
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)
|
||||
button :
|
||||
Theme
|
||||
-> List (Attribute msg)
|
||||
-> { isActive : Bool, onPress : msg, label : Element msg }
|
||||
-> Element msg
|
||||
actionButton attrs { isActive, onPress, label } =
|
||||
button theme attrs { isActive, onPress, label } =
|
||||
let
|
||||
( color, maybeEvent ) =
|
||||
if isActive then
|
||||
( blue, Just onPress )
|
||||
( (colors theme).primary, Just onPress )
|
||||
|
||||
else
|
||||
( lightGrey, Nothing )
|
||||
( (colors theme).disabled, Nothing )
|
||||
in
|
||||
Input.button
|
||||
([ padding 20
|
||||
, Background.color color
|
||||
, Font.color white
|
||||
]
|
||||
++ attrs
|
||||
(Background.color color
|
||||
:: Font.color (colors theme).buttonText
|
||||
:: attrs
|
||||
)
|
||||
{ onPress = maybeEvent
|
||||
, 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 :
|
||||
List (Attribute msg)
|
||||
-> String
|
||||
|
@ -97,11 +146,49 @@ heroText attrs s =
|
|||
el ([ Font.size fontSizes.huge ] ++ attrs) (text s)
|
||||
|
||||
|
||||
toDocument : { title : String, body : List (Element msg) } -> Document msg
|
||||
toDocument { title, body } =
|
||||
navBar : Theme -> List (Element msg) -> Element msg
|
||||
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
|
||||
, body =
|
||||
[ layout [] <|
|
||||
[ layout
|
||||
[ Font.color (colors theme).text
|
||||
, Background.color (colors theme).background
|
||||
]
|
||||
<|
|
||||
column [ width fill, height fill, spacing 20 ] body
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue