Homework-4/src/ClassSchedule/View.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 ]
]
]