Merge branch '439-uberarbeitung-favoriten-leiste' into 'master'

Überarbeitung, Favoriten-Leiste

Closes #439

See merge request uni2work/uni2work!36
This commit is contained in:
Gregor Kleen 2021-04-13 20:11:29 +00:00
commit 114e28c086
15 changed files with 279 additions and 65 deletions

View File

@ -221,7 +221,7 @@ cookies:
secure: "_env:COOKIES_SECURE:true" secure: "_env:COOKIES_SECURE:true"
user-defaults: user-defaults:
max-favourites: 12 max-favourites: 0
max-favourite-terms: 2 max-favourite-terms: 2
theme: Default theme: Default
date-time-format: "%a %d %b %Y %R" date-time-format: "%a %d %b %Y %R"

View File

@ -275,6 +275,9 @@ button:not(.btn-link),
display: grid display: grid
grid: min-content / auto-flow max-content grid: min-content / auto-flow max-content
.buttongroup--inline
display: inline-grid
input[type="submit"][disabled]:not(.btn-link), input[type="submit"][disabled]:not(.btn-link),
input[type="button"][disabled]:not(.btn-link), input[type="button"][disabled]:not(.btn-link),
button[disabled]:not(.btn-link), button[disabled]:not(.btn-link),
@ -328,6 +331,10 @@ input[type="button"].btn-info:not(.btn-link):hover,
&:not([disabled]):hover &:not([disabled]):hover
color: var(--color-link-hover) color: var(--color-link-hover)
// STACK ICON STYLE
.icon--stacked
font-size: 0.5rem
// GENERAL TABLE STYLES // GENERAL TABLE STYLES
.table .table
margin: 21px 0 margin: 21px 0

View File

@ -1,3 +1,6 @@
@use "../../common" as *
@use "../../app" as *
.main__aside .main__aside
position: fixed position: fixed
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
@ -90,6 +93,18 @@
padding: 0 13px padding: 0 13px
margin: 3px 0 margin: 3px 0
.asidenav__box-explanation
@extend .explanation
padding: 0 13px
margin: 3px 0
opacity: .66
font-size: .7rem
/* transition: opacity .2s ease, font-size .2s ease
/* &:hover
/* font-size: .9rem
/* opacity: 1
// LOGO // LOGO
.asidenav__logo .asidenav__logo
@ -217,9 +232,30 @@
.asidenav__link-shorthand .asidenav__link-shorthand
display: none display: none
.asidenav__link-favourite-toggle
opacity: .33
&:hover
opacity: 1
button
display: flex
text-decoration: none
.asidenav__link-label .asidenav__link-label
display: flex
justify-content: space-between
align-items: center
line-height: 1 line-height: 1
& > .asidenav__link-label-text
word-break: break-word
flex: 1 1 auto
& > .asidenav__link-favourite-toggle
flex: 0 0 $fa-fw-width
margin: 0 5px
// hover sub-menus // hover sub-menus
.asidenav__nested-list-wrapper .asidenav__nested-list-wrapper
position: absolute position: absolute

View File

@ -2682,6 +2682,8 @@ FavouriteParticipant: Ihre Kurse
FavouriteManual: Favoriten FavouriteManual: Favoriten
FavouriteCurrent: Aktueller Kurs FavouriteCurrent: Aktueller Kurs
FavouritesEmptyTip: Hier werden Ihre Kurse, sowie zuletzt besuchte Kurse angezeigt.
FavouritesToggleTip: Der Anzeigemodus für den aktuellen Kurs kann über einen Klick auf das Stern-Symbol zwischen automatisch, permanent und nie gewechselt werden.
FavouritesUnavailableTip: Das Schnellzugriffsmenü für diesen Kurs ist aktuell nicht verfügbar. FavouritesUnavailableTip: Das Schnellzugriffsmenü für diesen Kurs ist aktuell nicht verfügbar.
CourseEvents: Termine CourseEvents: Termine
@ -3228,4 +3230,4 @@ CourseSortingOnlyLoggedIn: Das Benutzerinterface zur Sortierung dieser Tabelle i
CorrectionInvisibleExamUnfinished: Die Frist „_{MsgExamFinished}“ für die relevante Prüfung ist noch nicht verstrichen CorrectionInvisibleExamUnfinished: Die Frist „_{MsgExamFinished}“ für die relevante Prüfung ist noch nicht verstrichen
CorrectionInvisibleRatingNotDone: Die Bewertung ist nicht als „Abgeschlossen“ markiert CorrectionInvisibleRatingNotDone: Die Bewertung ist nicht als „Abgeschlossen“ markiert
CorrectionInvisibleWarning: Die Bewertung dieser Abgabe ist aktuell für mindestens eine an der Abgabe beteiligte Person nicht sichtbar! CorrectionInvisibleWarning: Die Bewertung dieser Abgabe ist aktuell für mindestens eine an der Abgabe beteiligte Person nicht sichtbar!
CorrectionInvisibleReasons: Mögliche Gründe hierfür: CorrectionInvisibleReasons: Mögliche Gründe hierfür:

View File

@ -2682,6 +2682,8 @@ FavouriteParticipant: Your courses
FavouriteManual: Favourites FavouriteManual: Favourites
FavouriteCurrent: Current course FavouriteCurrent: Current course
FavouritesEmptyTip: Your courses and recently visited courses are shown here.
FavouritesToggleTip: The display mode for the current course can be changed between automatic, permanent and never with a click on the star symbol.
FavouritesUnavailableTip: Quick Actions for this course are currently not available. FavouritesUnavailableTip: Quick Actions for this course are currently not available.
CourseEvents: Occurrences CourseEvents: Occurrences

2
routes
View File

@ -174,7 +174,7 @@
!/course/new CourseNewR GET POST !lecturer !/course/new CourseNewR GET POST !lecturer
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer: /course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !tutor !corrector !exam-corrector !course-registered !course-time !evaluation !exam-office !allocation-admin / CShowR GET !tutor !corrector !exam-corrector !course-registered !course-time !evaluation !exam-office !allocation-admin
/favourite CFavouriteR POST /favourite CFavouriteR GET POST !free
/register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬course-registeredANDcourse-time !timeANDallocation-timeAND¬exam-resultANDcourse-registered !lecturerANDallocation-time /register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬course-registeredANDcourse-time !timeANDallocation-timeAND¬exam-resultANDcourse-registered !lecturerANDallocation-time
/register-template CRegisterTemplateR GET !course-time /register-template CRegisterTemplateR GET !course-time
/edit CEditR GET POST /edit CEditR GET POST

View File

@ -10,7 +10,6 @@ module Foundation.Instances
import Import.NoFoundation import Import.NoFoundation
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.List as List
import Data.List (inits) import Data.List (inits)
import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Core.Unsafe as Unsafe
@ -28,6 +27,7 @@ import qualified Foundation.Yesod.StaticContent as UniWorX
import qualified Foundation.Yesod.Persist as UniWorX import qualified Foundation.Yesod.Persist as UniWorX
import qualified Foundation.Yesod.Auth as UniWorX import qualified Foundation.Yesod.Auth as UniWorX
import Foundation.Instances.ButtonClass
import Foundation.SiteLayout import Foundation.SiteLayout
import Foundation.Type import Foundation.Type
import Foundation.I18n import Foundation.I18n
@ -51,29 +51,6 @@ import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
data instance ButtonClass UniWorX
= BCIsButton
| BCDefault
| BCPrimary
| BCSuccess
| BCInfo
| BCWarning
| BCDanger
| BCLink
| BCMassInputAdd | BCMassInputDelete
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance PathPiece (ButtonClass UniWorX) where
toPathPiece BCIsButton = "btn"
toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass
fromPathPiece = flip List.lookup $ map (toPathPiece &&& id) universeF
instance Button UniWorX ButtonSubmit where
btnClasses BtnSubmit = [BCIsButton, BCPrimary]
-- Please see the documentation for the Yesod typeclass. There are a number -- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here. -- of settings which can be configured by overriding methods here.
instance Yesod UniWorX where instance Yesod UniWorX where

View File

@ -0,0 +1,33 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Foundation.Instances.ButtonClass (ButtonClass(..)) where
import Import.NoFoundation
import Utils.Form
import Foundation.Type
import qualified Data.List as List
-- instance RenderMessage UniWorX ButtonSubmit
import Foundation.I18n ()
data instance ButtonClass UniWorX
= BCIsButton
| BCDefault
| BCPrimary
| BCSuccess
| BCInfo
| BCWarning
| BCDanger
| BCLink
| BCMassInputAdd | BCMassInputDelete
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance PathPiece (ButtonClass UniWorX) where
toPathPiece BCIsButton = "btn"
toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass
fromPathPiece = flip List.lookup $ map (toPathPiece &&& id) universeF
instance Button UniWorX ButtonSubmit where
btnClasses BtnSubmit = [BCIsButton, BCPrimary]

View File

@ -5,6 +5,7 @@ module Foundation.SiteLayout
( siteLayout', siteLayout ( siteLayout', siteLayout
, siteLayoutMsg', siteLayoutMsg , siteLayoutMsg', siteLayoutMsg
, getSystemMessageState , getSystemMessageState
, storedFavouriteReason
) where ) where
import Import.NoFoundation hiding (embedFile, runDB) import Import.NoFoundation hiding (embedFile, runDB)
@ -15,6 +16,7 @@ import Foundation.Routes
import Foundation.Navigation import Foundation.Navigation
import Foundation.I18n import Foundation.I18n
import Foundation.Yesod.Persist import Foundation.Yesod.Persist
import Foundation.Instances.ButtonClass
import Utils.SystemMessage import Utils.SystemMessage
import Utils.Form import Utils.Form
@ -37,6 +39,61 @@ import Text.Cassius (cassiusFile)
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
data CourseFavouriteToggleButton
= BtnCourseFavouriteToggleManual
| BtnCourseFavouriteToggleAutomatic
| BtnCourseFavouriteToggleOff
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''CourseFavouriteToggleButton $ camelToPathPiece' 4
instance Button UniWorX CourseFavouriteToggleButton where
btnLabel BtnCourseFavouriteToggleManual
= toWidget $ iconFixed IconCourseFavouriteManual
btnLabel BtnCourseFavouriteToggleAutomatic
= toWidget $ iconFixed IconCourseFavouriteAutomatic
btnLabel BtnCourseFavouriteToggleOff
= toWidget $ iconStacked IconCourseFavouriteManual IconCourseFavouriteOff
btnClasses _ = [BCIsButton, BCLink]
-- inspired by examAutoOccurrenceIgnoreRoomsForm
courseFavouriteToggleForm :: Maybe FavouriteReason -> Form ()
courseFavouriteToggleForm currentReason html
= over _1 void <$> identifyForm FIDCourseFavouriteToggle (buttonForm' [btn]) html
where
btn :: CourseFavouriteToggleButton
btn = case currentReason of
Nothing -> BtnCourseFavouriteToggleOff
(Just FavouriteVisited) -> BtnCourseFavouriteToggleAutomatic
(Just FavouriteParticipant) -> BtnCourseFavouriteToggleAutomatic
(Just FavouriteManual) -> BtnCourseFavouriteToggleManual
(Just FavouriteCurrent) -> BtnCourseFavouriteToggleAutomatic
-- (storedReason, isBlacklist)
-- Will never return FavouriteCurrent
-- Nothing if no entry for current user (e.g. not logged in)
storedFavouriteReason :: (MonadIO m, BearerAuthSite UniWorX) => TermId -> SchoolId -> CourseShorthand -> Maybe (AuthId UniWorX, AuthEntity UniWorX)
-> ReaderT SqlBackend m (Maybe (Maybe FavouriteReason, Bool))
storedFavouriteReason tid ssh csh muid = fmap unValueFirst . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do
E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse
E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid)
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.limit 1 -- we know that there is at most one match, but we tell the DB this info too
let isBlacklist = E.exists . E.from $ \courseNoFavourite ->
E.where_ $ E.just (courseNoFavourite E.^. CourseNoFavouriteUser) E.==. E.val (view _1 <$> muid)
E.&&. courseNoFavourite E.^. CourseNoFavouriteCourse E.==. course E.^. CourseId
reason :: (E.SqlExpr (E.Value (Maybe FavouriteReason)), E.SqlExpr (E.Value Bool))
reason = (courseFavourite E.?. CourseFavouriteReason, isBlacklist)
pure reason
where
unValueFirst :: [(E.Value (Maybe a), E.Value Bool)] -> Maybe (Maybe a, Bool)
-- `over each E.unValue` doesn't work here, since E.unValue is monomorphised
unValueFirst = fmap (bimap E.unValue E.unValue) . listToMaybe
data MemcachedKeyFavourites data MemcachedKeyFavourites
= MemcachedKeyFavouriteQuickActions (TermId, SchoolId, CourseShorthand) AuthContext (NonEmpty Lang) = MemcachedKeyFavouriteQuickActions (TermId, SchoolId, CourseShorthand) AuthContext (NonEmpty Lang)
@ -94,11 +151,11 @@ siteLayout' overrideHeading widget = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
muid <- maybeAuthPair
-- Lookup Favourites, Breadcrumbs, Headline, & Theme if possible -- Lookup Favourites, Breadcrumbs, Headline, & Theme if possible
(favourites', (title, parents), nav', contentHeadline, mmsgs, maxFavouriteTerms, currentTheme) <- do (favourites', (title, parents), nav', contentHeadline, mmsgs, maxFavouriteTerms, currentTheme, storedReasonAndToggleRoute) <- do
muid <- maybeAuthPair
(favCourses, breadcrumbs'', nav', contentHeadline, mmsgs) <- runDB $ do (favCourses, breadcrumbs'', nav', contentHeadline, mmsgs, storedReasonAndToggleRoute) <- runDB $ do
favCourses'' <- withReaderT (projectBackend @SqlReadBackend) . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do favCourses'' <- withReaderT (projectBackend @SqlReadBackend) . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do
E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse
E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid) E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid)
@ -189,8 +246,12 @@ siteLayout' overrideHeading widget = do
forM_ authTagPivots $ forM_ authTagPivots $
\authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute])) \authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute]))
getMessages getMessages
storedReasonAndToggleRoute <- case mcurrentRoute of
(Just (CourseR tid ssh csh _)) -> (, Just . SomeRoute $ CourseR tid ssh csh CFavouriteR) <$> storedFavouriteReason tid ssh csh muid
_otherwise -> pure (Nothing, Nothing)
return (favCourses, breadcrumbs'', nav', contentHeadline, mmsgs) return (favCourses, breadcrumbs'', nav', contentHeadline, mmsgs, storedReasonAndToggleRoute)
return ( favCourses return ( favCourses
, breadcrumbs'' , breadcrumbs''
@ -199,10 +260,37 @@ siteLayout' overrideHeading widget = do
, mmsgs , mmsgs
, maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid , maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid
, maybe userDefaultTheme userTheme $ view _2 <$> muid , maybe userDefaultTheme userTheme $ view _2 <$> muid
, storedReasonAndToggleRoute
) )
let (currentReason', maybeRoute) = storedReasonAndToggleRoute
currentReason = case currentReason' of
-- (reason, blacklist)
(Just (_reason, True)) -> Nothing
(Just (Just reason, False)) -> Just reason
(Just (Nothing, False)) -> Just FavouriteCurrent
Nothing -> Just FavouriteCurrent
showFavToggle :: FavouriteReason -> Bool
showFavToggle FavouriteCurrent = isJust muid
showFavToggle _favouriteReason = False
favouriteToggleRes <- runFormPost $ courseFavouriteToggleForm currentReason
let favouriteToggleWgt = favouriteToggleRes & \((_, favouriteToggleView), favouriteToggleEncoding) ->
wrapForm favouriteToggleView def
{ formAction = maybeRoute
, formEncoding = favouriteToggleEncoding
, formSubmit = FormNoSubmit
, formAttrs = [("class", "buttongroup buttongroup--inline")]
}
let favouriteTerms :: [TermIdentifier] let favouriteTerms :: [TermIdentifier]
favouriteTerms = take maxFavouriteTerms . Set.toDescList $ foldMap (\((_, tid, _, _), _, _, _, _) -> Set.singleton $ unTermKey tid) favourites' favouriteTerms = Set.toDescList . prune $ toTermKeySet favourites'
where
prune ts = currentTerms `Set.union` setTakeEnd (maxFavouriteTerms - Set.size currentTerms) (ts `Set.difference` currentTerms)
setTakeEnd n ts
| n <= 0 = Set.empty
| otherwise = Set.drop (Set.size ts - n) ts
currentTerms = toTermKeySet $ filter (views (_2 . _Value) . maybe True $ is _FavouriteCurrent) favourites'
toTermKeySet = setOf $ folded . _1 . _2 . to unTermKey
favourites <- fmap catMaybes . forM favourites' $ \(c@(_, tid, ssh, csh), E.Value mFavourite, courseVisible, mayView, mayEdit) favourites <- fmap catMaybes . forM favourites' $ \(c@(_, tid, ssh, csh), E.Value mFavourite, courseVisible, mayView, mayEdit)
-> let courseRoute = CourseR tid ssh csh CShowR -> let courseRoute = CourseR tid ssh csh CShowR
@ -251,6 +339,8 @@ siteLayout' overrideHeading widget = do
favouriteTermReason tid favReason' = favourites favouriteTermReason tid favReason' = favourites
& filter (\((_, tid', _, _), _, _, favReason, _, _, _) -> unTermKey tid' == tid && favReason == favReason') & filter (\((_, tid', _, _), _, _, favReason, _, _, _) -> unTermKey tid' == tid && favReason == favReason')
& sortOn (\((cName, _, _, _), _, _, _, _, _, _) -> cName) & sortOn (\((cName, _, _, _), _, _, _, _, _, _) -> cName)
anyFavToggle = flip any ((,) <$> universeF <*> favouriteTerms) $ \(reason, term) ->
showFavToggle reason && not (null $ favouriteTermReason term reason)
-- We break up the default layout into two components: -- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and -- default-layout is the contents of the body tag, and

View File

@ -185,7 +185,8 @@ updateFavourites cData = void . withReaderT projectBackend . runMaybeT $ do
User{userMaxFavourites} <- MaybeT $ get uid User{userMaxFavourites} <- MaybeT $ get uid
-- update Favourites -- update Favourites
for_ mcid $ \cid -> -- no need to store them with userMaxFavourites==0, since they will be removed in the pruning step anyway!
when (userMaxFavourites > 0) $ for_ mcid $ \cid ->
void . lift $ upsertBy void . lift $ upsertBy
(UniqueCourseFavourite uid cid) (UniqueCourseFavourite uid cid)
(CourseFavourite uid cid FavouriteVisited now) (CourseFavourite uid cid FavouriteVisited now)

View File

@ -4,6 +4,8 @@ module Handler.Course
import Import import Import
import qualified Database.Esqueleto as E
import qualified Database.Persist as P
import Handler.Course.Communication as Handler.Course import Handler.Course.Communication as Handler.Course
import Handler.Course.Delete as Handler.Course import Handler.Course.Delete as Handler.Course
@ -32,5 +34,35 @@ getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCNotesR = postCNotesR getCNotesR = postCNotesR
postCNotesR _ _ _ = defaultLayout [whamlet|You have corrector access to this course.|] postCNotesR _ _ _ = defaultLayout [whamlet|You have corrector access to this course.|]
-- simple redirect for now to avoid running into HTTP method not supported.
getCFavouriteR :: TermId -> SchoolId -> CourseShorthand -> Handler ()
getCFavouriteR tid ssh csh = redirect $ CourseR tid ssh csh CShowR
postCFavouriteR :: TermId -> SchoolId -> CourseShorthand -> Handler () postCFavouriteR :: TermId -> SchoolId -> CourseShorthand -> Handler ()
postCFavouriteR _ _ _ = error "not implemented" postCFavouriteR tid ssh csh = void $ do
authPair@(uid, _) <- requireAuthPair
runDB $ void $ do
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
now <- liftIO getCurrentTime
-- should never return FavouriteCurrent
newReason <- storedFavouriteReason tid ssh csh (Just authPair) <&> (\case
-- Maybe (Maybe reason, blacklist)
Nothing -> Just FavouriteManual
Just (_reason, True) -> Just FavouriteVisited
Just (Just FavouriteManual, False) -> Nothing
Just (_reason, False) -> Just FavouriteManual)
-- change stored reason in DB
case newReason of
(Just reason) -> do
void $ E.upsertBy
(UniqueCourseFavourite uid cid)
(CourseFavourite uid cid reason now)
[P.Update CourseFavouriteReason reason P.Assign]
E.deleteBy $ UniqueCourseNoFavourite uid cid
Nothing -> do
E.deleteBy $ UniqueCourseFavourite uid cid
void $ E.upsertBy
(UniqueCourseNoFavourite uid cid)
(CourseNoFavourite uid cid)
[] -- entry shouldn't exists, but keep it unchanged anyway
-- show course page again
redirect $ CourseR tid ssh csh CShowR

View File

@ -54,8 +54,6 @@ $(deriveSimpleWith ''ToMessage 'toMessage (over Text.packed $ Text.intercalate "
derivePersistField "Theme" derivePersistField "Theme"
data FavouriteReason data FavouriteReason
= FavouriteVisited = FavouriteVisited
| FavouriteParticipant | FavouriteParticipant
@ -68,6 +66,9 @@ deriveJSON defaultOptions
} ''FavouriteReason } ''FavouriteReason
derivePersistFieldJSON ''FavouriteReason derivePersistFieldJSON ''FavouriteReason
makePrisms ''FavouriteReason
data Sex data Sex
= SexNotKnown = SexNotKnown
| SexMale | SexMale

View File

@ -198,6 +198,7 @@ data FormIdentifier
| FIDmaterial | FIDmaterial
| FIDCourseNews | FIDCourseNews
| FIDCourseEvent | FIDCourseEvent
| FIDCourseFavouriteToggle
| FIDsubmission | FIDsubmission
| FIDsettings | FIDsettings
| FIDcorrectors | FIDcorrectors

View File

@ -37,6 +37,7 @@ data Icon
| IconVisible | IconVisible
| IconInvisible | IconInvisible
| IconCourse | IconCourse
| IconCourseFavouriteManual | IconCourseFavouriteAutomatic | IconCourseFavouriteOff
| IconEnrolTrue | IconEnrolTrue
| IconEnrolFalse | IconEnrolFalse
| IconPlanned | IconPlanned
@ -110,6 +111,9 @@ iconText = \case
IconVisible -> "eye" IconVisible -> "eye"
IconInvisible -> "eye-slash" IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap" IconCourse -> "graduation-cap"
IconCourseFavouriteManual -> "star"
IconCourseFavouriteAutomatic -> "star-half-alt"
IconCourseFavouriteOff -> "slash" -- TODO use FA regular style star for stacked icon
IconEnrolTrue -> "user-plus" IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash" IconEnrolFalse -> "user-slash"
IconPlanned -> "cog" IconPlanned -> "cog"
@ -189,6 +193,23 @@ icon ic = [shamlet|
$newline never $newline never
<i .fas .fa-#{iconText ic}> <i .fas .fa-#{iconText ic}>
|] |]
-- Create an icon from font-awesome with fixed width
iconFixed :: Icon -> Markup
iconFixed ic = [shamlet|
$newline never
<i .fas .fa-fw .fa-#{iconText ic}>
|]
-- Stack two icons from font-awesome without additional space
iconStacked :: Icon -> Icon -> Markup
iconStacked ic0 ic1
= [shamlet|
$newline never
<span .fa-stack .icon--stacked>
<i .fas .fa-stack-2x .fa-#{iconText ic0}>
<i .fas .fa-stack-2x .fa-#{iconText ic1}>
|]
-- Create an icon (defaults to "?") with a specified tooltip -- Create an icon (defaults to "?") with a specified tooltip
iconTooltip :: forall site. WidgetFor site () -> Maybe Icon -> Bool -> WidgetFor site () iconTooltip :: forall site. WidgetFor site () -> Maybe Icon -> Bool -> WidgetFor site ()

View File

@ -9,35 +9,46 @@ $newline never
_{MsgLogo} _{MsgLogo}
<div .asidenav> <div .asidenav>
$forall tid <- favouriteTerms $if null favouriteTerms && is _Just muid
<div .asidenav__box> <div .asidenav__box-explanation>
<h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right> _{MsgFavouritesEmptyTip}
<div .asidenav-term-identifier--long> $else
_{ShortTermIdentifier tid} $forall tid <- favouriteTerms
<div .asidenav-term-identifier--short> <div .asidenav__box>
#{toPathPiece tid} <h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right>
$forall favReason <- sortOn Down universeF <div .asidenav-term-identifier--long>
$if not (null $ favouriteTermReason tid favReason) _{ShortTermIdentifier tid}
<h3 .asidenav__box-subtitle> <div .asidenav-term-identifier--short>
_{favReason} #{toPathPiece tid}
<ul .asidenav__list.list--iconless> $forall favReason <- sortOn Down universeF
$forall ((cName, _, _, csh), courseRoute, mPageActions, _, courseVisible, _, mayEdit) <- favouriteTermReason tid favReason $if not (null $ favouriteTermReason tid favReason)
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active> <h3 .asidenav__box-subtitle>
<a .asidenav__link-wrapper href=@{courseRoute}> _{favReason}
<div .asidenav__link-shorthand>#{csh} <ul .asidenav__list.list--iconless>
<div .asidenav__link-label> $forall ((cName, _, _, csh), courseRoute, mPageActions, _, courseVisible, _, mayEdit) <- favouriteTermReason tid favReason
#{cName} <li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
$if mayEdit && not courseVisible <a .asidenav__link-wrapper href=@{courseRoute}>
\ #{iconInvisible} <div .asidenav__link-shorthand>#{csh}
<div .asidenav__nested-list-wrapper> <div .asidenav__link-label>
$maybe pageActions <- mPageActions <div .asidenav__link-label-text>
<ul .asidenav__nested-list.list--iconless> #{cName}
$forall (label, route) <- pageActions $if mayEdit && not courseVisible
<li .asidenav__nested-list-item> \ #{iconInvisible}
<a .asidenav__link-wrapper href=#{route}>#{label} $if showFavToggle favReason
$nothing <div .asidenav__link-favourite-toggle>
<p .asidenav__nested-list--unavailable> ^{favouriteToggleWgt}
_{MsgFavouritesUnavailableTip} <div .asidenav__nested-list-wrapper>
$maybe pageActions <- mPageActions
<ul .asidenav__nested-list.list--iconless>
$forall (label, route) <- pageActions
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>#{label}
$nothing
<p .asidenav__nested-list--unavailable>
_{MsgFavouritesUnavailableTip}
$if anyFavToggle
<div .asidenav__box-explanation>
_{MsgFavouritesToggleTip}
<div .asidenav__sigillum> <div .asidenav__sigillum>
<img src=@{StaticR img_lmu_sigillum_svg}> <img src=@{StaticR img_lmu_sigillum_svg}>