diff --git a/elm.json b/elm.json
index e680531..74964e8 100644
--- a/elm.json
+++ b/elm.json
@@ -9,10 +9,13 @@
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
+ "elm/http": "2.0.0",
+ "elm/json": "1.1.3",
"feathericons/elm-feather": "1.5.0"
},
"indirect": {
- "elm/json": "1.1.3",
+ "elm/bytes": "1.0.8",
+ "elm/file": "1.0.5",
"elm/svg": "1.0.1",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
diff --git a/index.html b/index.html
index f3d00f7..2230da5 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,7 @@
+
diff --git a/scss/style.scss b/scss/style.scss
index f7589c5..87de1db 100644
--- a/scss/style.scss
+++ b/scss/style.scss
@@ -22,7 +22,7 @@ body {
.split-elem {
flex-grow: 1;
- flex-shrink: 0;
+ flex-shrink: 1;
flex-basis: 40%;
margin: 0.25rem;
}
@@ -36,15 +36,20 @@ body {
margin-top: 1rem;
box-shadow: 0px 0px 5px rgba(grey, 0.1);
box-sizing: border-box;
+ width: 100%;
}
.class-table {
border-collapse: collapse;
+ table-layout: fixed;
th, td {
padding: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
th {
@@ -54,17 +59,9 @@ body {
tr {
transition: background-color 0.2s;
- &:nth-child(2n+3) {
- background-color: $off-color;
- }
-
&.hoverable:hover {
background-color: lighten(yellow, 30%);
}
-
- &.selected {
- border: 1px dashed black;
- }
}
}
@@ -87,24 +84,17 @@ body {
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 {
display: grid;
width: 100%;
height: 70vh;
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 {
@@ -114,11 +104,6 @@ body {
padding: 0.25rem;
box-sizing: border-box;
- &.selected {
- border-style: dashed;
- border-width: 2px;
- }
-
p {
margin-top: 0.1rem;
margin-bottom: 0.1rem;
@@ -146,3 +131,80 @@ body {
.time-column {
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%);
+}
diff --git a/src/ClassSchedule/Decode.elm b/src/ClassSchedule/Decode.elm
new file mode 100644
index 0000000..ce7c384
--- /dev/null
+++ b/src/ClassSchedule/Decode.elm
@@ -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)
diff --git a/src/ClassSchedule/Model.elm b/src/ClassSchedule/Model.elm
index 3af019a..b0a6a76 100644
--- a/src/ClassSchedule/Model.elm
+++ b/src/ClassSchedule/Model.elm
@@ -1,13 +1,8 @@
module ClassSchedule.Model exposing (..)
import Dict exposing (..)
+import Http exposing (Error)
-type Department
- = ComputerScience
- | Mathematics
- | Art
- | Biology
-
-type alias Crn = (Department, Int)
+type alias Crn = (String, Int)
type DayOfWeek
= Monday
@@ -28,6 +23,12 @@ flipDayHalf dh =
AM -> PM
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 hh (h, m, dh) =
if h + hh >= 12
@@ -59,6 +60,7 @@ type Msg
| SelectCourse Int
| AddCourse Int
| RemoveCourse Int
+ | ReceiveData (Result Error (List Course))
type alias Model =
{ terms : Dict String (List (CourseStatus, Course))
diff --git a/src/ClassSchedule/View.elm b/src/ClassSchedule/View.elm
index add26b8..652d473 100644
--- a/src/ClassSchedule/View.elm
+++ b/src/ClassSchedule/View.elm
@@ -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)
[ 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 (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 i = String.pad 2 '0' <| String.fromInt i
@@ -72,41 +64,89 @@ extractTimeCodes c =
in dayCodes ++ " " ++ viewTimeRange tr
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 sel i (cs, c) =
let
isSelected = sel == Just i
- addedIndicator =
- case cs of
- Added -> [ FeatherIcons.check |> FeatherIcons.toHtml [] ]
- NotAdded -> []
+ isAdded = cs == Added
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)] ]
- _ -> []
+ div [ class "course", onClick (SelectCourse i), classList [("selected", isSelected )] ]
+ [ div [ class "course-content" ]
+ [ p [ class "course-title" ] <|
+ [ text <| c.name ]
+ , p [ class "course-info" ] <|
+ viewAddedBubble isAdded ++
+ [ viewCrnBubble c.crn
+ , viewInstructorBubble c.instructors
+ , viewTimeBubble c
+ ]
+ ]
+ , 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
-viewClassTable sel =
- let
- header = tr []
- [ th [] [ ]
- , th [] [ text "Crn." ]
- , 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)
+viewVisibleClass : String -> Maybe Int -> Int -> (CourseStatus, Course) -> List (Html Msg)
+viewVisibleClass s mi i p = if courseContains s p then [ viewClass mi i p ] else []
+
+viewClassTable : String -> Maybe Int -> List (CourseStatus, Course) -> Html Msg
+viewClassTable s sel =
+ div [ class "table-wrapper" ] << List.concat << List.indexedMap (viewVisibleClass s sel)
viewToolbar : Model -> Html Msg
viewToolbar m = div []
@@ -114,18 +154,10 @@ viewToolbar m = div []
]
viewClassList : Model -> Html Msg
-viewClassList m = div []
- [ viewToolbar m
- , case get (m.term) (m.terms) of
- Just cs -> viewClassTable (m.selected) <| List.filter (courseContains m.searchInput) cs
+viewClassList m =
+ case get (m.term) (m.terms) of
+ Just cs -> viewClassTable m.searchInput m.selected cs
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 (h, m, th) = hour24 (h,m,th) * 60 + m
@@ -254,7 +286,7 @@ viewClassSchedule m =
let
header = span [] [] :: List.map viewTableDayHeader (List.take 5 days)
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)
container xs = div [ class "column-container" ] <|
List.map (\(c, ci) -> viewCourseBlock ci c) xs
@@ -268,7 +300,7 @@ viewModel : Model -> Html Msg
viewModel m = div [ class "main" ]
[ h1 [] [ text "Oregon State University Course Schedule" ]
, div [ class "split-pane" ]
- [ div [ class "split-elem" ] [ viewClassList m ]
+ [ div [ class "split-elem" ] [ viewToolbar m, viewClassList m ]
, div [ class "split-elem" ] [ viewClassSchedule m ]
]
]
diff --git a/src/Main.elm b/src/Main.elm
index aac00d8..3d9cc86 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -1,9 +1,11 @@
module Main exposing (..)
import ClassSchedule.Model exposing (..)
import ClassSchedule.View exposing (..)
+import ClassSchedule.Decode exposing (..)
import Browser exposing (Document, document)
import Dict exposing (..)
import Tuple exposing (..)
+import Http exposing (..)
oneHour : Time -> (Time, Time)
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 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 () = ({ 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 m =
@@ -82,6 +52,10 @@ update msg m =
AddCourse i -> (modifyCurrent (changeCourse i Added) m, Cmd.none)
RemoveCourse i -> (modifyCurrent (changeCourse i NotAdded) m, 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)
subscriptions : Model -> Sub Msg