307 lines
11 KiB
Elm
307 lines
11 KiB
Elm
module ClassSchedule.View exposing (..)
|
|
import ClassSchedule.Model exposing (..)
|
|
import Html exposing (Html, Attribute, div, text, table, td, th, tr, span, input, h1, p)
|
|
import Html.Attributes exposing (class, classList, type_, style)
|
|
import Html.Events exposing (onClick, onInput)
|
|
import Tuple exposing (..)
|
|
import Dict exposing (..)
|
|
import FeatherIcons
|
|
|
|
iconButton : String -> FeatherIcons.Icon -> String -> List (Attribute Msg) -> Html Msg
|
|
iconButton c i s attrs = span ([ class "button", class ("color-" ++ c) ] ++ attrs)
|
|
[ FeatherIcons.toHtml [] i, text s ]
|
|
|
|
viewCrn : Crn -> String
|
|
viewCrn (d, i) = d ++ " " ++ (String.pad 3 '0' <| String.fromInt i)
|
|
|
|
viewTimeNumber : Int -> String
|
|
viewTimeNumber i = String.pad 2 '0' <| String.fromInt i
|
|
|
|
viewDayHalf : DayHalf -> String
|
|
viewDayHalf dh =
|
|
case dh of
|
|
AM -> "AM"
|
|
PM -> "PM"
|
|
|
|
viewTime : Time -> String
|
|
viewTime (m, h, dh) = viewTimeNumber m ++ ":" ++ viewTimeNumber h ++ viewDayHalf dh
|
|
|
|
viewTimeRange : (Time, Time) -> String
|
|
viewTimeRange (t1, t2) = viewTime t1 ++ "-" ++ viewTime t2
|
|
|
|
viewDayCode : DayOfWeek -> String
|
|
viewDayCode dw =
|
|
case dw of
|
|
Monday -> "M"
|
|
Tuesday -> "T"
|
|
Wednesday -> "W"
|
|
Thursday -> "R"
|
|
Friday -> "F"
|
|
Saturday -> "S"
|
|
Sunday -> "U"
|
|
|
|
unique : List eq -> List eq
|
|
unique l =
|
|
case l of
|
|
[] -> []
|
|
(x::xs) -> x :: unique (List.filter ((/=) x) xs)
|
|
|
|
extractTimes : Course -> List (Time, Time)
|
|
extractTimes c =
|
|
let extractTime (d, t1, t2) = (t1, t2)
|
|
in unique <| List.map extractTime (c.times)
|
|
|
|
extractClasses : Course -> (Time, Time) -> List DayOfWeek
|
|
extractClasses c (t1, t2) =
|
|
let matches (d, tt1, tt2) = if tt1 == t1 && tt2 == t2 then Just d else Nothing
|
|
in List.filterMap matches (c.times)
|
|
|
|
extractTimeCodes : Course -> List String
|
|
extractTimeCodes c =
|
|
let
|
|
fromTime tr =
|
|
let dayCodes = String.concat <| List.map viewDayCode <| extractClasses c tr
|
|
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
|
|
isAdded = cs == Added
|
|
in
|
|
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)] ]
|
|
_ -> []
|
|
]
|
|
|
|
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 []
|
|
[ span [] [ text "Search: " , input [ type_ "text", onInput SearchInput ] [] ]
|
|
]
|
|
|
|
viewClassList : Model -> Html Msg
|
|
viewClassList m =
|
|
case get (m.term) (m.terms) of
|
|
Just cs -> viewClassTable m.searchInput m.selected cs
|
|
Nothing -> text "Please select a term!"
|
|
|
|
timeSortable : Time -> Int
|
|
timeSortable (h, m, th) = hour24 (h,m,th) * 60 + m
|
|
|
|
rangeSortable : (Time, Time) -> (Int, Int)
|
|
rangeSortable (t1, t2) = (timeSortable t1, -(timeSortable t2))
|
|
|
|
toHourCount : Time -> Time -> Float
|
|
toHourCount t1 t2 = toFloat (timeSortable t2 - timeSortable t1) / 60
|
|
|
|
maxTime : Time -> Time -> Time
|
|
maxTime t1 t2 = if timeSortable t1 > timeSortable t2 then t1 else t2
|
|
|
|
index : List a -> List (Int, a)
|
|
index =
|
|
let
|
|
indexWith n l =
|
|
case l of
|
|
[] -> []
|
|
(x::xs) -> (n,x) :: indexWith (n+1) xs
|
|
in indexWith 0
|
|
|
|
type alias SelectionReason = (CourseStatus, Bool)
|
|
|
|
findDay : Maybe Int -> DayOfWeek -> List (CourseStatus, Course) -> List (Course, SelectionReason, (Time, Time))
|
|
findDay ms dw l =
|
|
let
|
|
find (cs, b, c) = List.filter (\(ndw, _, _) -> ndw == dw) c.times
|
|
|> List.map (\(_, t1, t2) -> (c, (cs, b), (t1, t2)))
|
|
active (i, (cs, c)) =
|
|
if cs == Added || ms == Just i
|
|
then Just (cs, ms == Just i, c)
|
|
else Nothing
|
|
in
|
|
index l
|
|
|> List.filterMap active
|
|
|> List.concatMap find
|
|
|
|
findGroups : List (Course, SelectionReason, (Time, Time)) -> List (List (Course, SelectionReason, (Time, Time)))
|
|
findGroups cs =
|
|
let
|
|
findGroupsRec acc mt rcs =
|
|
case rcs of
|
|
[] ->
|
|
case acc of
|
|
[] -> []
|
|
_ -> [List.reverse acc]
|
|
((c, sr, (t1, t2))::trcs) ->
|
|
if timeSortable mt > timeSortable t1
|
|
then findGroupsRec ((c,sr,(t1,t2))::acc) (maxTime t2 mt) trcs
|
|
else List.reverse acc :: findGroupsRec [(c, sr, (t1, t2))] (maxTime t2 mt) trcs
|
|
in
|
|
findGroupsRec [] (0, 0, AM) <| List.sortBy (\(_, _, r) -> rangeSortable r) cs
|
|
|
|
type alias CellInfo =
|
|
{ width : Float
|
|
, height : Float
|
|
, verticalOffset : Float
|
|
, index : Int
|
|
, reason : SelectionReason
|
|
}
|
|
|
|
const : a -> b -> a
|
|
const x _ = x
|
|
|
|
mergeDicts : (a -> a -> a) -> Dict comparable a -> Dict comparable a -> Dict comparable a
|
|
mergeDicts f d1 d2 = Dict.merge Dict.insert (\k a b -> Dict.insert k (f a b)) Dict.insert d1 d2 Dict.empty
|
|
|
|
groupDims : List (Course, SelectionReason, (Time, Time)) -> Dict Int (List (Course, CellInfo))
|
|
groupDims l =
|
|
let
|
|
width = 100.0 / toFloat (List.length l)
|
|
courseInfo i (c, cr, (t1, t2)) = Dict.singleton (hour24 t1 - 8)
|
|
<| List.singleton
|
|
<| pair c
|
|
<|
|
|
{ width = width
|
|
, height = 100 * toHourCount t1 t2
|
|
, verticalOffset = let (_, m, _) = t1 in 100 * toFloat m / 60
|
|
, index = i
|
|
, reason = cr
|
|
}
|
|
in
|
|
List.foldl (mergeDicts (++)) Dict.empty <| List.indexedMap courseInfo l
|
|
|
|
days : List DayOfWeek
|
|
days = [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday]
|
|
|
|
courseContains : String -> (CourseStatus, Course) -> Bool
|
|
courseContains s (_, c) = String.contains s (viewCrn c.crn) || String.contains s c.name || List.any (String.contains s) c.instructors
|
|
|
|
buildCellDict : Maybe Int -> List (CourseStatus, Course) -> Dict Int (Dict Int (List (Course, CellInfo)))
|
|
buildCellDict ms l =
|
|
let
|
|
singleDay i dw = findDay ms dw l
|
|
|> findGroups
|
|
|> List.map groupDims
|
|
|> List.foldl (mergeDicts (++)) Dict.empty
|
|
|> Dict.singleton i
|
|
in
|
|
List.foldl Dict.union Dict.empty <| List.indexedMap singleDay days
|
|
|
|
viewCourseBlock : CellInfo -> Course -> Html Msg
|
|
viewCourseBlock ci c =
|
|
let
|
|
ps f = String.fromFloat f ++ "%"
|
|
nw = if (ci.width) == 100.0 then 100.0 else (ci.width)*0.95
|
|
(ad, sel) = ci.reason
|
|
in div
|
|
[ class "course-block"
|
|
, style "width" (ps nw)
|
|
, style "height" (ps (ci.height))
|
|
, style "left" (ps (toFloat ci.index * ci.width))
|
|
, style "top" (ps (ci.verticalOffset))
|
|
, classList [("selected", sel), ("color-green", ad == Added)]
|
|
]
|
|
[ p [] [ text <| viewCrn <| c.crn ]
|
|
, p [] [ text <| c.name ]
|
|
]
|
|
|
|
viewTableDayHeader : DayOfWeek -> Html Msg
|
|
viewTableDayHeader dw = span [ class "table-day-header" ] [ text <| viewDayCode dw ]
|
|
|
|
viewClassSchedule : Model -> Html Msg
|
|
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 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
|
|
in
|
|
div [ class "table-wrapper", class "schedule-table" ]
|
|
[ div [ class "week-grid" ] <|
|
|
header ++ (List.concatMap (\i -> span [ class "time-column" ] [ text <| viewTime <| from24 (i+8)] :: List.map container (time i)) times)
|
|
]
|
|
|
|
viewModel : Model -> Html Msg
|
|
viewModel m = div [ class "main" ]
|
|
[ h1 [] [ text "Oregon State University Course Schedule" ]
|
|
, div [ class "split-pane" ]
|
|
[ div [ class "split-elem" ] [ viewToolbar m, viewClassList m ]
|
|
, div [ class "split-elem" ] [ viewClassSchedule m ]
|
|
]
|
|
]
|