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 ] ] ]