Add redesign for Homework 4c.

This commit is contained in:
Danila Fedorin 2021-05-30 20:40:10 -07:00
parent 7b7b543416
commit 125d6391ba
7 changed files with 245 additions and 124 deletions

View File

@ -9,10 +9,13 @@
"elm/browser": "1.0.2", "elm/browser": "1.0.2",
"elm/core": "1.0.5", "elm/core": "1.0.5",
"elm/html": "1.0.0", "elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"feathericons/elm-feather": "1.5.0" "feathericons/elm-feather": "1.5.0"
}, },
"indirect": { "indirect": {
"elm/json": "1.1.3", "elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/svg": "1.0.1", "elm/svg": "1.0.1",
"elm/time": "1.0.0", "elm/time": "1.0.0",
"elm/url": "1.0.0", "elm/url": "1.0.0",

View File

@ -4,6 +4,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Lora&family=Roboto+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Lora&family=Roboto+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" media="screen" href="css/style.css"> <link rel="stylesheet" media="screen" href="css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>
<div id="#elm"> <div id="#elm">

View File

@ -22,7 +22,7 @@ body {
.split-elem { .split-elem {
flex-grow: 1; flex-grow: 1;
flex-shrink: 0; flex-shrink: 1;
flex-basis: 40%; flex-basis: 40%;
margin: 0.25rem; margin: 0.25rem;
} }
@ -36,15 +36,20 @@ body {
margin-top: 1rem; margin-top: 1rem;
box-shadow: 0px 0px 5px rgba(grey, 0.1); box-shadow: 0px 0px 5px rgba(grey, 0.1);
box-sizing: border-box; box-sizing: border-box;
width: 100%;
} }
.class-table { .class-table {
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
th, td { th, td {
padding: 0.5rem; padding: 0.5rem;
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
th { th {
@ -54,17 +59,9 @@ body {
tr { tr {
transition: background-color 0.2s; transition: background-color 0.2s;
&:nth-child(2n+3) {
background-color: $off-color;
}
&.hoverable:hover { &.hoverable:hover {
background-color: lighten(yellow, 30%); background-color: lighten(yellow, 30%);
} }
&.selected {
border: 1px dashed black;
}
} }
} }
@ -87,24 +84,17 @@ body {
vertical-align: middle; vertical-align: middle;
} }
.color-green {
background-color: lighten(#5bc275, 30%);
border-color: #5bc275;
color: darken(#5bc275, 30%);
}
.color-red {
background-color: lighten(#c25b75, 30%);
border-color: #c25b75;
color: darken(#c25b75, 30%);
}
.week-grid { .week-grid {
display: grid; display: grid;
width: 100%; width: 100%;
height: 70vh; height: 70vh;
grid-template-columns: auto repeat(5, 1fr); grid-template-columns: auto repeat(5, 1fr);
grid-template-rows: auto repeat(10, 1fr); grid-template-rows: auto repeat(12, 1fr);
.selected {
border-style: dashed;
border-width: 2px;
}
} }
.course-block { .course-block {
@ -114,11 +104,6 @@ body {
padding: 0.25rem; padding: 0.25rem;
box-sizing: border-box; box-sizing: border-box;
&.selected {
border-style: dashed;
border-width: 2px;
}
p { p {
margin-top: 0.1rem; margin-top: 0.1rem;
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
@ -146,3 +131,80 @@ body {
.time-column { .time-column {
padding-right: 1rem; padding-right: 1rem;
} }
.bubble {
padding: 0.2rem;
padding-right: 0.5rem;
padding-left: 0.5rem;
border: 0.1rem solid;
border-radius: 0.5rem;
margin-right: 0.5rem;
background-color: lighten(#86b2ff, 10%);
border-color: #86b2ff;
white-space: nowrap;
}
.course {
padding: 1rem;
display: flex;
&.selected {
border-style: dashed;
border-width: 2px;
}
&:nth-child(2n) {
background-color: $off-color;
}
&.selected {
border: 1px dashed black;
}
p {
margin: 0;
}
}
.course-content {
flex-grow: 1;
}
.course-add-remove {
display: flex;
flex-direction: column;
justify-content: center;
}
.course-title {
font-weight: bold;
font-size: 1.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 40vw;
}
p.course-info {
margin-top: 0.5rem;
display: flex;
}
.feather {
height: 1.3rem;
stroke-width: 0.15rem;
margin-right: 0.1rem;
vertical-align: middle;
}
.color-green {
background-color: lighten(#5bc275, 30%);
border-color: #5bc275;
color: darken(#5bc275, 30%);
}
.color-red {
background-color: lighten(#c25b75, 30%);
border-color: #c25b75;
color: darken(#c25b75, 30%);
}

View File

@ -0,0 +1,47 @@
module ClassSchedule.Decode exposing (..)
import ClassSchedule.Model exposing (..)
import Json.Decode exposing (..)
dayOfWeek : Decoder DayOfWeek
dayOfWeek =
let
runDayOfWeek s =
case s of
"M" -> succeed Monday
"T" -> succeed Tuesday
"W" -> succeed Wednesday
"R" -> succeed Thursday
"F" -> succeed Friday
_ -> fail "Not a valid day of the week!"
in
string |> andThen runDayOfWeek
duration : Decoder (Time, Time)
duration =
let mkRange i j = (from24 i, addMinutes (round (j*60)) (from24 i))
in field "hour" int |> andThen (\i -> field "duration" float |> map (mkRange i))
time : Decoder (DayOfWeek, Time, Time)
time = map2 (\x (y, z) -> (x, y, z))
(field "day" dayOfWeek)
duration
crn : Decoder Crn
crn = string
|> andThen (\s ->
case String.split " " s of
[c, is] ->
case String.toInt is of
Just i -> succeed (c, i)
Nothing -> fail "Invalid course number!"
_ -> fail "Invalid course code!")
course : Decoder Course
course = map4 Course
(field "code" crn)
(field "title" string)
(field "instructor" (map List.singleton string))
(field "times" (list time))
response : Decoder (List Course)
response = field "courses" (list course)

View File

@ -1,13 +1,8 @@
module ClassSchedule.Model exposing (..) module ClassSchedule.Model exposing (..)
import Dict exposing (..) import Dict exposing (..)
import Http exposing (Error)
type Department type alias Crn = (String, Int)
= ComputerScience
| Mathematics
| Art
| Biology
type alias Crn = (Department, Int)
type DayOfWeek type DayOfWeek
= Monday = Monday
@ -28,6 +23,12 @@ flipDayHalf dh =
AM -> PM AM -> PM
PM -> AM PM -> AM
hour24 : Time -> Int
hour24 (h, _, th) = if th == AM then h else h + 12
from24 : Int -> Time
from24 i = (modBy 12 i, 0, if i >= 12 then PM else AM)
addHours : Int -> Time -> Time addHours : Int -> Time -> Time
addHours hh (h, m, dh) = addHours hh (h, m, dh) =
if h + hh >= 12 if h + hh >= 12
@ -59,6 +60,7 @@ type Msg
| SelectCourse Int | SelectCourse Int
| AddCourse Int | AddCourse Int
| RemoveCourse Int | RemoveCourse Int
| ReceiveData (Result Error (List Course))
type alias Model = type alias Model =
{ terms : Dict String (List (CourseStatus, Course)) { terms : Dict String (List (CourseStatus, Course))

View File

@ -11,16 +11,8 @@ iconButton : String -> FeatherIcons.Icon -> String -> List (Attribute Msg) -> Ht
iconButton c i s attrs = span ([ class "button", class ("color-" ++ c) ] ++ attrs) iconButton c i s attrs = span ([ class "button", class ("color-" ++ c) ] ++ attrs)
[ FeatherIcons.toHtml [] i, text s ] [ FeatherIcons.toHtml [] i, text s ]
viewDept : Department -> String
viewDept d =
case d of
ComputerScience -> "CS"
Mathematics -> "MTH"
Art -> "ART"
Biology -> "BIO"
viewCrn : Crn -> String viewCrn : Crn -> String
viewCrn (d, i) = viewDept d ++ " " ++ (String.pad 3 '0' <| String.fromInt i) viewCrn (d, i) = d ++ " " ++ (String.pad 3 '0' <| String.fromInt i)
viewTimeNumber : Int -> String viewTimeNumber : Int -> String
viewTimeNumber i = String.pad 2 '0' <| String.fromInt i viewTimeNumber i = String.pad 2 '0' <| String.fromInt i
@ -72,41 +64,89 @@ extractTimeCodes c =
in dayCodes ++ " " ++ viewTimeRange tr in dayCodes ++ " " ++ viewTimeRange tr
in List.map fromTime <| extractTimes c in List.map fromTime <| extractTimes c
-- viewClass : Maybe Int -> Int -> (CourseStatus, Course) -> Html Msg
-- viewClass sel i (cs, c) =
-- let
-- isSelected = sel == Just i
-- addedIndicator =
-- case cs of
-- Added -> [ FeatherIcons.check |> FeatherIcons.toHtml [] ]
-- NotAdded -> []
-- in
-- tr [ class "hoverable", onClick (SelectCourse i), classList [("selected", isSelected)] ]
-- [ td [] addedIndicator
-- , td [] [ text <| viewCrn (c.crn) ]
-- , td [] [ text <| c.name ]
-- , td [] [ text <| String.join ", " <| c.instructors ]
-- , td [] [ text <| String.join ", " <| extractTimeCodes c ]
-- , td [] <|
-- case (isSelected, cs) of
-- (True, NotAdded) -> [ iconButton "green" (FeatherIcons.check) "Add" [ onClick (AddCourse i)] ]
-- (True, Added) -> [ iconButton "red" (FeatherIcons.x) "Remove" [onClick (RemoveCourse i)] ]
-- _ -> []
-- ]
crnIcon : Html Msg
crnIcon = FeatherIcons.code |> FeatherIcons.toHtml []
instructorIcon : Html Msg
instructorIcon = FeatherIcons.user |> FeatherIcons.toHtml []
instructorsIcon : Html Msg
instructorsIcon = FeatherIcons.users |> FeatherIcons.toHtml []
timeIcon : Html Msg
timeIcon = FeatherIcons.clock |> FeatherIcons.toHtml []
checkIcon : Html Msg
checkIcon = FeatherIcons.check |> FeatherIcons.toHtml []
viewCrnBubble : Crn -> Html Msg
viewCrnBubble crn = span [ class "bubble" ] [ crnIcon, text <| viewCrn crn ]
viewTimeBubble : Course -> Html Msg
viewTimeBubble c = span [ class "bubble" ] [ timeIcon, text <| String.join ", " <| extractTimeCodes c ]
viewAddedBubble : Bool -> List (Html Msg)
viewAddedBubble b = if b then [ span [ class "bubble", class "color-green" ] [ checkIcon, text "Added!" ] ] else []
viewInstructorBubble : List String -> Html Msg
viewInstructorBubble is = span [ class "bubble" ] <|
case is of
[] -> [ instructorIcon, text <| "(no instructor assigned)" ]
[i] -> [ instructorIcon, text <| i ]
_ -> [ instructorsIcon, text <| String.join ", " is ]
viewClass : Maybe Int -> Int -> (CourseStatus, Course) -> Html Msg viewClass : Maybe Int -> Int -> (CourseStatus, Course) -> Html Msg
viewClass sel i (cs, c) = viewClass sel i (cs, c) =
let let
isSelected = sel == Just i isSelected = sel == Just i
addedIndicator = isAdded = cs == Added
case cs of
Added -> [ FeatherIcons.check |> FeatherIcons.toHtml [] ]
NotAdded -> []
in in
tr [ class "hoverable", onClick (SelectCourse i), classList [("selected", isSelected)] ] div [ class "course", onClick (SelectCourse i), classList [("selected", isSelected )] ]
[ td [] addedIndicator [ div [ class "course-content" ]
, td [] [ text <| viewCrn (c.crn) ] [ p [ class "course-title" ] <|
, td [] [ text <| c.name ] [ text <| c.name ]
, td [] [ text <| String.join ", " <| c.instructors ] , p [ class "course-info" ] <|
, td [] [ text <| String.join ", " <| extractTimeCodes c ] viewAddedBubble isAdded ++
, td [] <| [ viewCrnBubble c.crn
case (isSelected, cs) of , viewInstructorBubble c.instructors
(True, NotAdded) -> [ iconButton "green" (FeatherIcons.check) "Add" [ onClick (AddCourse i)] ] , viewTimeBubble c
(True, Added) -> [ iconButton "red" (FeatherIcons.x) "Remove" [onClick (RemoveCourse i)] ] ]
_ -> [] ]
, div [ class "course-add-remove" ] <|
case (isSelected, cs) of
(True, NotAdded) -> [ iconButton "green" (FeatherIcons.check) "Add" [ onClick (AddCourse i)] ]
(True, Added) -> [ iconButton "red" (FeatherIcons.x) "Remove" [onClick (RemoveCourse i)] ]
_ -> []
] ]
viewClassTable : Maybe Int -> List (CourseStatus, Course) -> Html Msg viewVisibleClass : String -> Maybe Int -> Int -> (CourseStatus, Course) -> List (Html Msg)
viewClassTable sel = viewVisibleClass s mi i p = if courseContains s p then [ viewClass mi i p ] else []
let
header = tr [] viewClassTable : String -> Maybe Int -> List (CourseStatus, Course) -> Html Msg
[ th [] [ ] viewClassTable s sel =
, th [] [ text "Crn." ] div [ class "table-wrapper" ] << List.concat << List.indexedMap (viewVisibleClass s sel)
, th [] [ text "Course Name" ]
, th [] [ text "Instructors" ]
, th [] [ text "Times" ]
, th [] []
]
in
div [ class "table-wrapper" ] << List.singleton << table [ class "class-table" ] << (::) header << List.indexedMap (viewClass sel)
viewToolbar : Model -> Html Msg viewToolbar : Model -> Html Msg
viewToolbar m = div [] viewToolbar m = div []
@ -114,18 +154,10 @@ viewToolbar m = div []
] ]
viewClassList : Model -> Html Msg viewClassList : Model -> Html Msg
viewClassList m = div [] viewClassList m =
[ viewToolbar m case get (m.term) (m.terms) of
, case get (m.term) (m.terms) of Just cs -> viewClassTable m.searchInput m.selected cs
Just cs -> viewClassTable (m.selected) <| List.filter (courseContains m.searchInput) cs
Nothing -> text "Please select a term!" Nothing -> text "Please select a term!"
]
hour24 : Time -> Int
hour24 (h, _, th) = if th == AM then h else h + 12
from24 : Int -> Time
from24 i = (modBy 12 i, 0, if i >= 12 then PM else AM)
timeSortable : Time -> Int timeSortable : Time -> Int
timeSortable (h, m, th) = hour24 (h,m,th) * 60 + m timeSortable (h, m, th) = hour24 (h,m,th) * 60 + m
@ -254,7 +286,7 @@ viewClassSchedule m =
let let
header = span [] [] :: List.map viewTableDayHeader (List.take 5 days) header = span [] [] :: List.map viewTableDayHeader (List.take 5 days)
cellDict = buildCellDict m.selected (Maybe.withDefault [] <| Dict.get m.term m.terms) cellDict = buildCellDict m.selected (Maybe.withDefault [] <| Dict.get m.term m.terms)
times = List.range 0 10 times = List.range 0 12
time h = List.map (\w -> Maybe.withDefault [] <| Maybe.andThen (Dict.get h) <| Dict.get w cellDict) (List.range 0 4) time h = List.map (\w -> Maybe.withDefault [] <| Maybe.andThen (Dict.get h) <| Dict.get w cellDict) (List.range 0 4)
container xs = div [ class "column-container" ] <| container xs = div [ class "column-container" ] <|
List.map (\(c, ci) -> viewCourseBlock ci c) xs List.map (\(c, ci) -> viewCourseBlock ci c) xs
@ -268,7 +300,7 @@ viewModel : Model -> Html Msg
viewModel m = div [ class "main" ] viewModel m = div [ class "main" ]
[ h1 [] [ text "Oregon State University Course Schedule" ] [ h1 [] [ text "Oregon State University Course Schedule" ]
, div [ class "split-pane" ] , div [ class "split-pane" ]
[ div [ class "split-elem" ] [ viewClassList m ] [ div [ class "split-elem" ] [ viewToolbar m, viewClassList m ]
, div [ class "split-elem" ] [ viewClassSchedule m ] , div [ class "split-elem" ] [ viewClassSchedule m ]
] ]
] ]

View File

@ -1,9 +1,11 @@
module Main exposing (..) module Main exposing (..)
import ClassSchedule.Model exposing (..) import ClassSchedule.Model exposing (..)
import ClassSchedule.View exposing (..) import ClassSchedule.View exposing (..)
import ClassSchedule.Decode exposing (..)
import Browser exposing (Document, document) import Browser exposing (Document, document)
import Dict exposing (..) import Dict exposing (..)
import Tuple exposing (..) import Tuple exposing (..)
import Http exposing (..)
oneHour : Time -> (Time, Time) oneHour : Time -> (Time, Time)
oneHour t = (t, addMinutes 50 t) oneHour t = (t, addMinutes 50 t)
@ -20,46 +22,14 @@ nPm i = (i, 0, PM)
onDays : List DayOfWeek -> (Time, Time) -> List (DayOfWeek, Time, Time) onDays : List DayOfWeek -> (Time, Time) -> List (DayOfWeek, Time, Time)
onDays dds (t1, t2) = List.map (\d -> (d, t1, t2)) dds onDays dds (t1, t2) = List.map (\d -> (d, t1, t2)) dds
classes : List Course
classes =
[ { crn = (ComputerScience, 517)
, name = "Theory of Comp"
, instructors = ["Mike Rosulek"]
, times = onDays [Monday, Wednesday, Friday] <| oneHour <| nPm 2
}
, { crn = (ComputerScience, 531)
, name = "Artificial Intelligence"
, instructors = ["John Doe"]
, times = onDays [Monday, Wednesday, Friday] <| oneHour <| nPm 0
}
, { crn = (ComputerScience, 533)
, name = "Intelligent Somethings"
, instructors = ["Alan Fern"]
, times = onDays [Monday, Wednesday] <| twoHours <| nAm 10
}
, { crn = (ComputerScience, 535)
, name = "Deep Learning"
, instructors = ["F. Li"]
, times = onDays [Tuesday, Thursday] <| twoHours <| nPm 2
}
, { crn = (ComputerScience, 551)
, name = "Computer Graphics"
, instructors = ["Mike Bailey"]
, times = onDays [Monday, Wednesday] <| twoHours <| nPm 0
}
, { crn = (ComputerScience, 565)
, name = "Human-Computer Interaction"
, instructors = ["Minsuk Kahng"]
, times = onDays [Tuesday, Thursday] <| twoHours <| nPm 4
}
]
terms : Dict String (List (CourseStatus, Course))
terms = Dict.singleton "Spring 2021"
<| List.map (pair NotAdded) classes
init : Flags -> (Model, Cmd Msg) init : Flags -> (Model, Cmd Msg)
init () = ({ terms = terms, term = "Spring 2021", searchInput = "", selected = Nothing }, Cmd.none) init () =
( { terms = Dict.empty, term = "Spring 2021", searchInput = "", selected = Nothing }
, Http.get
{ url = "cs565_hw4c_schedule_data.json"
, expect = expectJson ReceiveData response
}
)
view : Model -> Document Msg view : Model -> Document Msg
view m = view m =
@ -82,6 +52,10 @@ update msg m =
AddCourse i -> (modifyCurrent (changeCourse i Added) m, Cmd.none) AddCourse i -> (modifyCurrent (changeCourse i Added) m, Cmd.none)
RemoveCourse i -> (modifyCurrent (changeCourse i NotAdded) m, Cmd.none) RemoveCourse i -> (modifyCurrent (changeCourse i NotAdded) m, Cmd.none)
SearchInput s -> ({ m | searchInput = s }, Cmd.none) SearchInput s -> ({ m | searchInput = s }, Cmd.none)
ReceiveData e ->
case e of
Err er -> Debug.log (Debug.toString er) (m, Cmd.none)
Ok d -> ({ m | terms = Dict.singleton "Spring 2021" <| List.map (pair NotAdded) d }, Cmd.none)
_ -> (m, Cmd.none) _ -> (m, Cmd.none)
subscriptions : Model -> Sub Msg subscriptions : Model -> Sub Msg