feat: navbar header containers

BREAKING CHANGE: major navigation refactor
This commit is contained in:
Gregor Kleen 2020-01-23 22:35:16 +01:00
parent 4c58699d1f
commit 1348c91c3c
50 changed files with 1993 additions and 1691 deletions

View File

@ -160,7 +160,7 @@ h4
--current-header-height: var(--header-height-collapsed)
position: relative
background-color: white
transition: padding-left .2s ease-out
transition: padding-left .2s ease-out, margin-top 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
margin-top: var(--current-header-height)
margin-left: 0
@ -173,6 +173,9 @@ h4
> .container
margin: 20px 0
.navbar__container-radio:checked ~ * &
margin-top: calc(var(--current-header-height) + 40px)
.main__content, .modal__content
a
text-decoration: underline
@ -228,6 +231,7 @@ input[type="submit"],
input[type="button"],
button,
.btn
font-family: var(--font-base)
outline: 0
border: 0
box-shadow: 0
@ -639,6 +643,9 @@ section
z-index: 19
pointer-events: none
.navbar__container-radio:checked ~ &
top: calc(80px + var(--header-height))
@media (max-width: 768px)
.ribbon
top: calc(20px + var(--header-height-collapsed))

View File

@ -1,48 +1,89 @@
import { Utility } from '../../core/utility';
import './navbar.sass';
import * as throttle from 'lodash.throttle';
export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]';
const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized';
export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper';
const HEADER_CONTAINER_INITIALIZED_CLASS = '.navbar-header-container--initialized';
@Utility({
selector: LANGUAGE_SELECT_UTIL_SELECTOR,
selector: HEADER_CONTAINER_UTIL_SELECTOR,
})
export class LanguageSelectUtil {
export class NavHeaderContainerUtil {
_element;
checkbox;
radioButton;
closeButton;
container;
wasOpen;
_throttleUpdateWasOpen;
constructor(element) {
if (!element) {
throw new Error('Language Select utility needs to be passed an element!');
throw new Error('Navbar Header Container utility needs to be passed an element!');
}
if (element.classList.contains(LANGUAGE_SELECT_INITIALIZED_CLASS)) {
if (element.classList.contains(HEADER_CONTAINER_INITIALIZED_CLASS)) {
return false;
}
this._element = element;
this.checkbox = element.querySelector('#lang-checkbox');
this.radioButton = document.getElementById(`${this._element.id}-radio`);
if (!this.radioButton) {
throw new Error('Navbar Header Container utility could not find associated radio button!');
}
window.addEventListener('click', event => this.close(event));
this.closeButton = document.getElementById('container-radio-none');
if (!this.closeButton) {
throw new Error('Navbar Header Container utility could not find radio button for closing!');
}
this.container = document.getElementById(`${this._element.id}-container`);
if (!this.container) {
throw new Error('Navbar Header Container utility could not find associated container!');
}
element.classList.add(LANGUAGE_SELECT_INITIALIZED_CLASS);
const closer = this.container.querySelector('.navbar__container-list-closer');
if (closer) {
closer.classList.add('navbar__container-list-closer--hidden');
}
this.updateWasOpen();
this.throttleUpdateWasOpen = throttle(this.updateWasOpen.bind(this), 100, { leading: false, trailing: true });
this._element.classList.add(HEADER_CONTAINER_INITIALIZED_CLASS);
}
close(event) {
if (!this._element.contains(event.target) && window.document.contains(event.target)) {
this.checkbox.checked = false;
start() {
window.addEventListener('click', this.clickHandler.bind(this));
this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this));
}
clickHandler() {
if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) {
this.close();
}
}
destroy() {
// TODO
close() {
this.radioButton.checked = false;
this.closeButton.checked = true;
this.throttleUpdateWasOpen();
}
isOpen() {
return this.radioButton.checked;
}
updateWasOpen() {
this.wasOpen = this.isOpen();
}
destroy() { /* TODO */ }
}
export const NavbarUtils = [
LanguageSelectUtil,
NavHeaderContainerUtil,
];

View File

@ -21,28 +21,115 @@
.navbar
position: fixed
display: flex
flex-direction: row
align-items: center
justify-content: flex-start
right: 0
top: 0
left: var(--asidenav-width-xl)
height: var(--header-height)
min-height: var(--header-height)
background-color: var(--color-primary)
color: white
z-index: 20
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2)
overflow: auto
transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
margin: 0
padding: 10px 0
@media (max-width: 1199px)
.navbar
@media (max-width: 1199px)
left: var(--asidenav-width-lg)
@media (max-width: 768px)
.navbar
@media (max-width: 768px), (max-height: 500px)
left: 0
min-height: var(--header-height-collapsed)
padding: 0
@media (max-width: 768px), (max-height: 500px)
.navbar__link-wrapper
height: var(--header-height-collapsed)
.navbar__list-wrapper
display: flex
flex-flow: row nowrap
justify-content: space-between
align-items: center
margin: 0 40px
.navbar__list-left
margin-right: 40px
.navbar__list
display: flex
flex-flow: row nowrap
justify-content: flex-end
align-items: center
list-style-type: none
&.navbar__list-left
justify-content: flex-start
& > *
display: block
.navbar__container-list
position: relative
width: 100%
/* margin: 10px 0 0 0 */
padding: 0 40px
overflow: hidden
& > ul
display: flex
flex-flow: row nowrap
align-items: center
overflow: auto
list-style-type: none
justify-content: flex-end
& > *
display: block
margin-right: 12px
&:last-child
margin-right: 0
&.navbar__container-list--left > ul
justify-content: flex-start
@media (max-width: 768px), (max-height: 500px)
padding: 0
visibility: collapse
margin: 0
height: 0
transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1), margin 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
.navbar__container-list-closer
position: absolute
top: 5px
right: 10px
width: 20px
height: 20px
text-align: center
transform-origin: 10px 10px
transform: rotate(-0.25turn)
&.navbar__container-list--left
transform: rotate(0.25turn)
opacity: 0.5
transition: transform 0.2s, opacity 0.2s ease
&:hover
opacity: 1
transform: scale(1.4)
&.navbar__container-list-closer--hidden
visibility: hidden
&.navbar__container-list--left .navbar__container-list-closer
left: 14.5px
right: auto
// links
.navbar__link-wrapper
@ -67,12 +154,13 @@
padding: 2px 4px
text-transform: uppercase
font-weight: 600
font-size: 16px
@media (min-width: 769px)
@media (min-width: 769px) and (min-height: 501px)
.navbar__link-wrapper
border: 1px solid rgba(255, 255, 255, 0.7)
@media (max-width: 768px)
@media (max-width: 768px), (max-height: 500px)
.navbar__link-wrapper
box-shadow: none
min-width: 0
@ -86,22 +174,42 @@
transform: scale(0.65)
margin-bottom: 0
// navbar list
.navbar__list
white-space: nowrap
.navbar__container-link
display: block
@media (min-width: 769px) and (min-height: 501px)
border: 1px solid rgba(255, 255, 255, 0.7)
text-decoration: none
+ .navbar__list
margin-left: 12px
@media (max-width: 768px), (max-height: 500px)
text-decoration: underline
@media (min-width: 769px)
.navbar__list:last-of-type
padding-right: 40px
height: 30px
color: var(--color-lightwhite) !important
background-color: rgba(0, 0, 0, 0) !important
padding: 5px 10px
text-transform: uppercase
font-weight: 600
font-size: 16px
outline: 0
min-width: 0
transition: none
cursor: pointer
&:not(.navbar__container-link--active):hover
text-decoration: none
@media (min-width: 769px) and (min-height: 501px)
background-color: var(--color-dark) !important
color: var(--color-lightwhite) !important
&.navbar__container-link--active
text-decoration: none
@media (min-width: 769px) and (min-height: 501px)
background-color: var(--color-lightwhite) !important
color: var(--color-dark) !important
@media (max-width: 768px)
.navbar__list
+ .navbar__list
margin-left: 0
padding-right: 40px
// list item
.navbar__list-item
@ -124,34 +232,10 @@
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper
margin-left: 0
.navbar__list-left
flex: 5
padding-left: 40px
@media (max-width: 768px)
.navbar__list-left
padding-left: 0
// "Favorites" list item, only visible on small screens and logged in
.navbar__list-item
&.navbar__list-item--favorite
display: none
.navbar__list-item--favorite
display: none
background-color: var(--color-primary)
.logged-in
.navbar__list
li.navbar__list-item--favorite,
.navbar__list-item--favorite
display: inline-block
@media (min-width: 426px)
.logged-in
.navbar__list
.navbar__list-item--favorite
display: none !important
.navbar__list-item--favorite
display: none !important
.navbar__list-item--active
background-color: var(--color-lightwhite)
@ -163,7 +247,7 @@
.navbar__list-item--active .navbar__link-wrapper
color: var(--color-dark)
.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper, #lang-checkbox:checked ~ * .navbar__link-wrapper
.navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper
background-color: var(--color-dark)
color: var(--color-lightwhite)
@ -186,43 +270,5 @@
display: block
height: var(--header-height-collapsed)
@media (max-width: 768px)
.navbar,
.navbar__pushdown
height: var(--header-height-collapsed)
.navbar__link-wrapper
height: var(--header-height-collapsed)
@media (max-height: 500px)
.navbar,
.navbar__pushdown
height: var(--header-height-collapsed)
.navbar__link-wrapper
height: var(--header-height-collapsed)
#lang-dropdown
.navbar__container-radio--none, .navbar__container-radio
display: none
position: fixed
top: var(--header-height)
right: 0
min-width: 200px
z-index: 10
background-color: white
border-radius: 2px
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
select
display: block
button
display: block
width: 100%
#lang-checkbox:checked ~ #lang-dropdown
display: block
@media (max-width: 768px)
#lang-dropdown
top: var(--header-height-collapsed)

View File

@ -493,7 +493,7 @@ CorrectorsPlaceholder: Korrektoren...
CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert.
Users: Benutzer
HomeHeading: Aktuelle Termine
NewsHeading: Aktuelles
LoginHeading: Authentifizierung
LoginTitle: Authentifizierung
ProfileHeading: Benutzereinstellungen
@ -508,9 +508,9 @@ NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen f
TokensLastReset: Tokens zuletzt invalidiert
TokensResetSuccess: Authorisierungs-Tokens invalidiert
HomeOpenAllocations: Offene Zentralanmeldungen
HomeUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Prüfungen
NewsOpenAllocations: Offene Zentralanmeldungen
NewsUpcomingSheets: Anstehende Übungsblätter
NewsUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
CloseAlert: Schliessen
@ -1114,7 +1114,7 @@ InvalidRoute: Konnte URL nicht interpretieren
MenuOpenCourses: Kurse mit offener Registrierung
MenuOpenAllocations: Aktive Zentralanmeldungen
MenuHome: Aktuell
MenuNews: Aktuell
MenuInformation: Informationen
MenuLegal: Rechtliche Informationen
MenuDataProt: Datenschutzerklärung
@ -1561,7 +1561,6 @@ ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten
ExamBonusManual': Manuelle Berechnung
ExamGradesExplanation: Diese Ansicht zeigt die selben Daten an, wie die Tabelle von Prüfungsteilnehmern. Anpassen der Teilnehmerdaten und Ergebnisse ist nur dort möglich. Hier können Sie vor Allem einsehen und markieren, welche Prüfungsleistungen von den zuständigen Prüfungsbeauftragten bereits vollständig bearbeitet wurden.
ExamRegisterForOccurrence: Anmeldung zur Klausur erfolgt durch Anmeldung zu einem Termin/Raum

View File

@ -491,7 +491,7 @@ CorrectorsPlaceholder: Correctors...
CorrectorsDefaulted: List of correctors was automatically generated based on those of preceding sheets for this course. No data has been saved, yet.
Users: Users
HomeHeading: Home
NewsHeading: News
LoginHeading: Authentication
LoginTitle: Authentication
ProfileHeading: Settings
@ -506,9 +506,9 @@ NotificationSettingsHeading displayName: Notification settings for #{displayName
TokensLastReset: Tokens last reset
TokensResetSuccess: Successfully invalidated all authorisation tokens
HomeOpenAllocations: Active central allocations
HomeUpcomingSheets: Upcoming exercise sheets
HomeUpcomingExams: Upcoming exams
NewsOpenAllocations: Active central allocations
NewsUpcomingSheets: Upcoming exercise sheets
NewsUpcomingExams: Upcoming exams
NumCourses num: #{num} #{pluralEN num "course" "courses"}
CloseAlert: Close
@ -1113,7 +1113,7 @@ InvalidRoute: Could not interpret url
MenuOpenCourses: Courses with open registration
MenuOpenAllocations: Active central allocations
MenuHome: Home
MenuNews: News
MenuInformation: Information
MenuLegal: Legal
MenuDataProt: Data protection
@ -1559,7 +1559,6 @@ ExamBonusRule: Bonus points from exercises
ExamNoBonus': No automatic exam bonus
ExamBonusPoints': Compute from exercise achievements
ExamBonusManual': Manual computation
ExamGradesExplanation: This view shows the same data as the table of exam participants. Changing participant's data and achievements is only possible via the table of exam participants. Primarily, this view allows you to check and adjust which exam achievements were properly handled by the relevant exam offices.
ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room

2
routes
View File

@ -41,7 +41,7 @@
/metrics MetricsR GET
/ HomeR GET !free
/ NewsR GET !free
/users UsersR GET POST -- no tags, i.e. admins only
/users/#CryptoUUIDUser AdminUserR GET POST
/users/#CryptoUUIDUser/delete AdminUserDeleteR POST

View File

@ -101,7 +101,7 @@ import Data.List (cycle)
-- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Home
import Handler.News
import Handler.Info
import Handler.Help
import Handler.Profile

File diff suppressed because it is too large Load Diff

View File

@ -168,7 +168,7 @@ instance RenderMessage UniWorX MsgLanguage where
| ("en" : _) <- lang' = mr MsgEnglish
| otherwise = lang
where
mr = renderMessage foundation ls
mr = renderMessage foundation $ lang : filter (/= lang) ls
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel

View File

@ -1089,5 +1089,6 @@ postEUsersR tid ssh csh examn = do
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading
let computedValuesTip = $(i18nWidgetFile "exam-users/computed-values-tip")
let computedValuesTip = notificationWidget NotificationBroad Warning
$(i18nWidgetFile "exam-users/computed-values-tip")
$(widgetFile "exam-users")

View File

@ -441,4 +441,5 @@ postEGradesR tid ssh csh examn = do
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading
let examGradesExplanation = notificationWidget NotificationBroad Info $(i18nWidgetFile "exam-office/exam-grades-explanation")
$(widgetFile "exam-office/exam-results")

View File

@ -30,4 +30,5 @@ postEEGradesR tid ssh coursen examn = do
siteLayoutMsg (MsgExternalExamGrades coursen examn) $ do
setTitleI MsgBreadcrumbExternalExamGrades
let examGradesExplanation = notificationWidget NotificationBroad Info $(i18nWidgetFile "exam-office/exam-grades-explanation")
$(widgetFile "exam-office/externalExamGrades")

View File

@ -80,7 +80,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
return res
invitationSuccessMsg _ _ =
return $ SomeMessage MsgExamOfficeUserInvitationAccepted
invitationUltDest _ _ = return $ SomeRoute HomeR
invitationUltDest _ _ = return $ SomeRoute NewsR
makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId))

View File

@ -1,4 +1,4 @@
module Handler.Home where
module Handler.News where
import Import
@ -9,21 +9,25 @@ import Database.Esqueleto.Utils.TH
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
getHomeR :: Handler Html
getHomeR = do
getNewsR :: Handler Html
getNewsR = do
muid <- maybeAuthId
defaultLayout $ do
setTitleI MsgHomeHeading
setTitleI MsgNewsHeading
when (is _Nothing muid) $
notificationWidget NotificationBroad Info $(i18nWidgetFile "pitch")
case muid of
Just uid -> do
homeUpcomingExams uid
homeUpcomingSheets uid
newsUpcomingExams uid
newsUpcomingSheets uid
Nothing ->
$(i18nWidgetFile "unauth-home")
$(i18nWidgetFile "unauth-news")
homeUpcomingSheets :: UserId -> Widget
homeUpcomingSheets uid = do
newsUpcomingSheets :: UserId -> Widget
newsUpcomingSheets uid = do
cTime <- liftIO getCurrentTime
let tableData :: E.LeftOuterJoin
(E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant)) (E.SqlExpr (Entity Course))) (E.SqlExpr (Entity Sheet)))
@ -121,11 +125,11 @@ homeUpcomingSheets uid = do
, dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing
}
$(widgetFile "home/upcomingSheets")
$(widgetFile "news/upcomingSheets")
homeUpcomingExams :: UserId -> Widget
homeUpcomingExams uid = do
newsUpcomingExams :: UserId -> Widget
newsUpcomingExams uid = do
now <- liftIO getCurrentTime
((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do
User {userWarningDays} <- get404 uid
@ -255,6 +259,6 @@ homeUpcomingExams uid = do
(, userWarningDays) <$> dbTable examDBTableValidator examDBTable
$(widgetFile "home/upcomingExams")
$(widgetFile "news/upcomingExams")

View File

@ -850,17 +850,14 @@ postCsvOptionsR = do
, formAttrs = [ asyncSubmitAttr | isModal ]
}
postLangR :: Handler ()
postLangR :: Handler Void
postLangR = do
((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm
requestedLang <- selectLanguage' appLanguages . hoistMaybe <$> lookupGlobalPostParam PostLanguage
lang' <- runDB . updateUserLanguage $ Just requestedLang
formResult langRes $ \(lang, route) -> do
lang' <- runDB . updateUserLanguage $ Just lang
app <- getYesod
let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang''
| otherwise = renderMessage app []
addMessage Success . toHtml $ mr MsgLanguageChanged
redirect route
app <- getYesod
let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang''
| otherwise = renderMessage app []
addMessage Success . toHtml $ mr MsgLanguageChanged
invalidArgs ["Language form required"]
redirect . fromMaybe NewsR =<< lookupGlobalGetParam GetReferer

View File

@ -399,7 +399,7 @@ invitationR' InvitationConfig{..} = liftHandler $ do
Nothing -> do
addMessageI Info MsgInvitationDeclined
deleteBy . UniqueInvitation itEmail $ invRef @junction fid
return . Just $ SomeRoute HomeR
return . Just $ SomeRoute NewsR
Just (jData, formCtx) -> do
let junction = review _InvitableJunction (invitee, fid, jData)
mResult <- invitationInsertHook itEmail fEnt iData junction formCtx $ insertUniqueEntity junction

View File

@ -10,6 +10,7 @@ import Model.Submission as Import
import Model.Tokens as Import
import Utils.Tokens as Import
import Utils.Frontend.Modal as Import
import Utils.Frontend.Notification as Import
import Utils.Lens as Import
import Settings as Import

View File

@ -46,6 +46,7 @@ import Data.Scientific
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Utils
import Utils.Frontend.Notification
-- import Utils.Message
-- import Utils.PathPiece
-- import Utils.Route
@ -869,26 +870,15 @@ wformMessage :: (MonadHandler m) => Message -> WForm m ()
wformMessage = void . aFormToWForm . aformMessage
formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site)
formMessage Message{..} = do
formMessage msg = do
return (FormSuccess (), FieldView
{ fvLabel = mempty
, fvTooltip = Nothing
, fvId = idFormMessageNoinput
, fvErrors = Nothing
, fvRequired = False
, fvInput = [whamlet|
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{maybe defaultIcon iconText messageIcon}>
<div .notification__content>
#{messageContent}
|]
, fvInput = notification NotificationNarrow msg
})
where
defaultIcon = case messageStatus of
Success -> "check-circle"
Info -> "info-circle"
Warning -> "exclamation-circle"
Error -> "exclamation-triangle"
---------------------
-- Form evaluation --

View File

@ -0,0 +1,43 @@
module Utils.Frontend.Notification
( NotificationType(..)
, notification
, notificationWidget
) where
import ClassyPrelude.Yesod
import Settings
import Utils.Message
import Utils.Icon
import Control.Lens
import Control.Lens.Extras (is)
data NotificationType
= NotificationNarrow
| NotificationBroad
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
makePrisms ''NotificationType
notification :: NotificationType
-> Message
-> WidgetFor site ()
notification nType Message{ messageIcon = messageIcon', .. }
= $(widgetFile "widgets/notification")
where
messageIcon = fromMaybe defaultIcon messageIcon'
defaultIcon = case messageStatus of
Success -> IconNotificationSuccess
Info -> IconNotificationInfo
Warning -> IconNotificationWarning
Error -> IconNotificationError
notificationWidget :: Yesod site
=> NotificationType
-> MessageStatus
-> WidgetFor site ()
-> WidgetFor site ()
notificationWidget nType ms = notification nType <=< messageWidget ms

View File

@ -63,44 +63,58 @@ data Icon
| IconApplicationVeto
| IconApplicationFiles
| IconTooltipDefault
deriving (Eq, Ord, Enum, Bounded, Show, Read)
| IconNotificationSuccess
| IconNotificationInfo
| IconNotificationWarning
| IconNotificationError
| IconFavourite
| IconLanguage
| IconNavContainerClose
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable)
iconText :: Icon -> Text
iconText = \case
IconNew -> "seedling"
IconOK -> "check"
IconNotOK -> "times"
IconWarning -> "exclamation"
IconProblem -> "bolt"
IconVisible -> "eye"
IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap"
IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash"
IconPlanned -> "cog"
IconAnnounce -> "bullhorn"
IconExam -> "poll-h"
IconExamRegisterTrue -> "calendar-check"
IconExamRegisterFalse -> "calendar-times"
IconCommentTrue -> "comment-alt"
IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free
IconLink -> "link"
IconFileDownload -> "file-download"
IconFileUpload -> "file-upload"
IconFileZip -> "file-archive"
IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash"
IconApplicationVeto -> "times"
IconApplicationFiles -> "file-alt"
IconTooltipDefault -> "question-circle"
IconNew -> "seedling"
IconOK -> "check"
IconNotOK -> "times"
IconWarning -> "exclamation"
IconProblem -> "bolt"
IconVisible -> "eye"
IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap"
IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash"
IconPlanned -> "cog"
IconAnnounce -> "bullhorn"
IconExam -> "poll-h"
IconExamRegisterTrue -> "calendar-check"
IconExamRegisterFalse -> "calendar-times"
IconCommentTrue -> "comment-alt"
IconCommentFalse -> "comment-alt-slash"
IconLink -> "link"
IconFileDownload -> "file-download"
IconFileUpload -> "file-upload"
IconFileZip -> "file-archive"
IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash"
IconApplicationVeto -> "times"
IconApplicationFiles -> "file-alt"
IconTooltipDefault -> "question-circle"
IconNotificationSuccess -> "check-circle"
IconNotificationInfo -> "info-circle"
IconNotificationWarning -> "exclamation-circle"
IconNotificationError -> "exclamation-triangle"
IconFavourite -> "star"
IconLanguage -> "flag-alt"
IconNavContainerClose -> "chevron-up"
instance Universe Icon
instance Finite Icon

View File

@ -1,5 +1,6 @@
module Utils.Lens.TH
( makeLenses_, makeClassyFor_
( lensRules_
, makeLenses_, makeClassyFor_
, multifocusG, multifocusL
) where

View File

@ -6,7 +6,7 @@ module Utils.Message
, addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget
, statusToUrgencyClass
, Message(..)
, messageIconI
, messageIconI, messageIconIHamlet, messageIconWidget
, messageI, messageIHamlet, messageFile, messageWidget, messageTooltip
) where
@ -163,6 +163,15 @@ messageIHamlet ms iHamlet = do
let mi = Nothing
Message ms <$> withUrlRenderer (iHamlet $ toHtml . mr) <*> pure mi
messageIconIHamlet :: ( MonadHandler m
, RenderMessage (HandlerSite m) msg
, HandlerSite m ~ site
) => MessageStatus -> Icon -> HtmlUrlI18n msg (Route site) -> m Message
messageIconIHamlet messageStatus (Just -> messageIcon) iHamlet = do
mr <- getMessageRender
messageContent <- withUrlRenderer (iHamlet $ toHtml . mr)
return Message{..}
addMessageFile :: MessageStatus -> FilePath -> ExpQ
addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|]
@ -189,6 +198,15 @@ messageWidget mc wgt = do
PageContent{pageBody} <- liftHandler $ widgetToPageContent wgt
messageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site))
messageIconWidget :: forall m site.
( MonadHandler m
, HandlerSite m ~ site
, Yesod site
) => MessageStatus -> Icon -> WidgetFor site () -> m Message
messageIconWidget ms mi wgt = do
PageContent{pageBody} <- liftHandler $ widgetToPageContent wgt
messageIconIHamlet ms mi (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site))
getMessages :: MonadHandler m => m [Message]
getMessages = fmap decodeMessage <$> ClassyPrelude.Yesod.getMessages

View File

@ -58,6 +58,7 @@ data GlobalPostParam = PostFormIdentifier
| PostDBCsvImportAction
| PostLoginDummy
| PostExamAutoOccurrencePrevious
| PostLanguage
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe GlobalPostParam

View File

@ -1,5 +1,11 @@
$newline never
$if not isModal
$with containers <- filter isNavHeaderContainer nav
$if not (null containers)
<input name=nav-container type=radio .navbar__container-radio--none checked #container-radio-none>
$forall (_, containerIdent, _, _) <- containers
<input name=nav-container type=radio .navbar__container-radio ##{containerIdent}-radio>
<!-- secondary navigation at the side -->
^{asidenav}
<!-- navigation -->
@ -15,7 +21,7 @@ $if not isModal
$if not isModal
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
$if not $ Just NewsR == mcurrentRoute
^{breadcrumbsWgt}
<div .main__content-body>

View File

@ -3,7 +3,5 @@ $newline never
^{closeWgt}
<section>
$if hasUsers
<div .notification .notification-info .fa-question .notification--broad>
<div .notification__content>
_{MsgExamGradesExplanation}
^{examGradesExplanation}
^{examUsersTable}

View File

@ -1,6 +1,4 @@
$newline never
$if hasUsers
<div .notification .notification-info .fa-question .notification--broad>
<div .notification__content>
_{MsgExamGradesExplanation}
^{examGradesExplanation}
^{table}

View File

@ -0,0 +1,9 @@
$newline never
<p>
Diese Ansicht zeigt die selben Daten an, wie die Tabelle von Prüfungsteilnehmern.<br />
Anpassen der Teilnehmerdaten und Ergebnisse ist nur dort möglich.
<p>
Hier können Sie vor Allem einsehen und markieren, welche #
Prüfungsleistungen von den zuständigen Prüfungsbeauftragten bereits #
vollständig bearbeitet wurden.

View File

@ -0,0 +1,9 @@
$newline never
<p>
This view shows the same data as the table of exam participants.<br />
Changing participant's data and achievements is only possible via #
the table of exam participants.
<p>
Primarily, this view allows you to check and adjust which exam #
achievements were properly handled by the relevant exam offices.

View File

@ -1,24 +1,22 @@
$newline never
<div .notification .notification-warning .fa-exclamation-triangle .notification--broad>
<div .notification__content>
<p>
Die Tabelle enthält Werte, die automatisch berechnet wurden.
<p>
Automatisch berechnete Werte (Bonus und Prüfungsergebnis) werden weder dem #
entsprechenden Teilnehmer angezeigt, noch an das Prüfungsamt gemeldet #
bevor sie manuell übernommen wurden.<br />
Hierzu können Sie die Aktion „Berechnetes Prüfungsergebnis übernehmen“ #
verwenden.
<p>
Sie können die automatisch berechneten Werte auch manuell (via CSV-Import) #
überschreiben.<br />
Wenn die so gesetzten Werte nicht den automatisch Berechneten entsprechen #
sind sie <i>inkonsistent</i>.
<p>
Automatisch berechnete Werte sind gekennzeichnet wie folgt:
<p>
Die Tabelle enthält Werte, die automatisch berechnet wurden.
<p>
Automatisch berechnete Werte (Bonus und Prüfungsergebnis) werden weder dem #
entsprechenden Teilnehmer angezeigt, noch an das Prüfungsamt gemeldet #
bevor sie manuell übernommen wurden.<br />
Hierzu können Sie die Aktion „Berechnetes Prüfungsergebnis übernehmen“ #
verwenden.
<p>
Sie können die automatisch berechneten Werte auch manuell (via CSV-Import) #
überschreiben.<br />
Wenn die so gesetzten Werte nicht den automatisch Berechneten entsprechen #
sind sie <i>inkonsistent</i>.
<p>
Automatisch berechnete Werte sind gekennzeichnet wie folgt:
<table style="font-weight: normal">
<tr>
<td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatisch berechnet
<td style="padding: 0 7px" .table__td>Normaler Wert
<td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inkonsistent
<table style="font-weight: normal">
<tr>
<td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatisch berechnet
<td style="padding: 0 7px" .table__td>Normaler Wert
<td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inkonsistent

View File

@ -1,23 +1,21 @@
$newline never
<div .notification .notification-warning .fa-exclamation-triangle .notification--broad>
<div .notification__content>
<p>
This table contains values that were computed automatically.
<p>
Values computed automatically (bonus and result) are shown to neither the #
participant nor relevant exam offices until they are manually accepted.<br />
To do this you may use the action “Accept computed result”.
<p>
You are also able to override the automatically computed values manually #
(via CSV import).<br />
<p>
This table contains values that were computed automatically.
<p>
Values computed automatically (bonus and result) are shown to neither the #
participant nor relevant exam offices until they are manually accepted.<br />
To do this you may use the action “Accept computed result”.
<p>
You are also able to override the automatically computed values manually #
(via CSV import).<br />
If values thus overriden do not match the automatically computed values #
they are considered <i>inconsistent</i>.
<p>
Automatically computed values are marked as follows:
If values thus overriden do not match the automatically computed values #
they are considered <i>inconsistent</i>.
<p>
Automatically computed values are marked as follows:
<table style="font-weight: normal">
<tr>
<td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatically computed
<td style="padding: 0 7px" .table__td>Normal value
<td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inconsistent
<table style="font-weight: normal">
<tr>
<td style="padding: 0 7px 0 0" .table__td .table__td--automatic>Automatically computed
<td style="padding: 0 7px" .table__td>Normal value
<td style="padding: 0 0 0 7px" .table__td .table__td--overriden>Inconsistent

View File

@ -0,0 +1,11 @@
$newline never
<p>
Uni2work ist ein Lehrverwaltungssystem, welches an der #
Ludwig-Maximilians-Universität München entwickelt und eingesetzt #
wird.
<p>
Insbesondere unterstützt Uni2work teilnehmende Institute bei #
Übungsbetrieb, Prüfungs- und Notenverwaltung und bietet vollständige #
Kurshomepages inkl. Vorlesungsmaterial und Terminen.

View File

@ -0,0 +1,8 @@
$newline never
<p>
Uni2work is a teaching management system, developed and deployed at #
Ludwig-Maximilians-Universität München.
<p>
Uni2work supports participating departments in managing their course #
exercises, exams and exam achievements, and provides complete course #
homepages including course material and dates.

View File

@ -1,6 +1,6 @@
$newline never
<section>
<h2>_{MsgHomeUpcomingExams}
<h2>_{MsgNewsUpcomingExams}
$if hasExams
^{examTable}
$else

View File

@ -1,4 +1,4 @@
$newline never
<section>
<h2>_{MsgHomeUpcomingSheets}
<h2>_{MsgNewsUpcomingSheets}
^{sheetTable}

View File

@ -28,12 +28,9 @@ $newline never
<div .asidenav__link-label>#{courseName}
<div .asidenav__nested-list-wrapper>
<ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
$case menuItemType
$of PageActionPrime
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _
$forall (NavLink{navLabel}, route) <- pageActions
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{navLabel}
<div .asidenav__sigillum>
<img src=@{StaticR img_lmu_sigillum_svg}>

View File

@ -1,63 +0,0 @@
.breadcrumbs__container {
position: relative;
color: var(--color-lightwhite);
padding: 4px 13px;
background-color: var(--color-dark);
line-height: 30px;
}
@media (min-width: 426px) {
.breadcrumbs__container {
padding: 7px 20px;
}
}
@media (min-width: 769px) {
.breadcrumbs__container {
padding: 7px 40px;
}
}
a.breadcrumbs__link {
color: var(--color-lightwhite);
&:hover {
color: var(--color-white);
}
}
.breadcrumbs__item {
padding-right: 14px;
position: relative;
line-height: 28px;
opacity: 0.8;
z-index: 1;
margin-right: 10px;
&:hover {
opacity: 1;
}
&::after {
content: '';
position: absolute;
top: 11px;
right: 0;
width: 7px;
height: 7px;
border-style: solid;
border-width: 0;
border-bottom-width: 1px;
border-right-width: 1px;
border-color: var(--color-white);
transform: rotate(-45deg);
z-index: 10;
opacity: 1;
}
}
.breadcrumbs__last-item {
line-height: 28px;
vertical-align: bottom;
font-weight: 600;
}

View File

@ -1,13 +1,6 @@
$newline never
<footer .footer>
<div .footer-links>
$forall (MenuItem{menuItemType, menuItemRoute = _, menuItemIcon = _, menuItemLabel, menuItemModal = _}, menuIdent, route) <- menuTypes
$case menuItemType
$of Footer
$# Not used but available (remove ` = _` from the pattern match above, as needed):
$# highlight (urlRoute menuItemRoute) :: Bool -- ^ Is this menu item currently active (i.e.: are we on this page)
$# menuItemModal :: Bool -- ^ Should this menu item open a modal instead of being a normal link
$# menuItemIcon :: Maybe Text -- ^ Should this menu item have an icon, if yes, then the name of the icon
<a href=#{route} ##{menuIdent}>
_{SomeMessage menuItemLabel}
$of _
$forall (NavFooter{ navLink }, navIdent, navRoute', _) <- filter isNavFooter nav
<a href=#{navRoute'} ##{navIdent}>
_{navLink}

View File

@ -0,0 +1,11 @@
##{containerIdent}-radio:checked ~ * ##{containerIdent}-container
visibility: visible
margin: 14px 0 0 0
height: 30px
##{containerIdent}-radio:checked ~ * ##{containerIdent}
background-color: var(--color-dark)
color: var(--color-lightwhite)
.navbar__link-icon
opacity: 1

View File

@ -0,0 +1,5 @@
$newline never
<label .navbar__link-wrapper for=#{navIdent}-radio ##{navIdent}>
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{iconText navIcon}>
<div .navbar__link-label>_{SomeMessage navLabel}

View File

@ -1,6 +1,5 @@
$newline never
<a .navbar__link-wrapper href=#{route} ##{menuIdent}>
$if isJust menuItemIcon
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{fromMaybe "none" menuItemIcon}>
<div .navbar__link-label>_{SomeMessage menuItemLabel}
<a .navbar__link-wrapper href=#{route} ##{ident}>
<div .navbar__link-icon>
<i .fas .fa-2x .fa-#{iconText navIcon}>
<div .navbar__link-label>_{SomeMessage navLabel}

View File

@ -0,0 +1,7 @@
$newline never
$maybe csrf <- csrfToken
<input type=hidden name=#{defaultCsrfParamName} value=#{csrf}>
$forall (k, v) <- navData
<input type=hidden name=#{k} value=#{v}>
<button .navbar__container-link :highlightNav iN:.navbar__container-link--active type=submit>
_{SomeMessage navLabel}

View File

@ -0,0 +1,3 @@
$newline never
<a .navbar__container-link :highlightNav iN:.navbar__container-link--active href=#{route} ##{ident}>
_{SomeMessage navLabel}

View File

@ -1,48 +1,54 @@
$newline never
<div .navbar-container>
<div .navbar-shadow>
<nav .navbar.js-sticky-navbar>
<ul .navbar__list.list--inline.navbar__list-left>
$# manually add favorites to navbar for small screens
<li .navbar__list-item.navbar__list-item--favorite>
<a .navbar__link-wrapper href="#">
<div .navbar__link-icon>
<i .fas .fa-2x .fa-star>
<div .navbar__link-label>_{MsgNavigationFavourites}
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes
$case menuItemType
$of NavbarAside
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal
^{navbarModal (menuItem, menuIdent)}
$else
^{navbarItem (menuItem, menuIdent)}
$of _
<ul .navbar__list.list--inline>
$forall (menuItem@MenuItem{menuItemType, menuItemRoute, menuItemModal}, menuIdent, _) <- menuTypes
$case menuItemType
$of NavbarRight
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal
^{navbarModal (menuItem, menuIdent)}
$else
^{navbarItem (menuItem, menuIdent)}
$of NavbarSecondary
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal
^{navbarModal (menuItem, menuIdent)}
$else
^{navbarItem (menuItem, menuIdent)}
$of _
<li .navbar__list-item--lang-wrapper uw-language-select>
<input type="checkbox" id="lang-checkbox" uw-no-checkbox>
<div id="lang-dropdown">
^{langFormView'}
<div .navbar__list-item .navbar__list-item--language>
<label .navbar__link-wrapper for="lang-checkbox">
<nav .navbar>
<div .navbar__list-wrapper>
<ul .navbar__list.navbar__list-left>
$# manually add favorites to navbar for small screens
<li .navbar__list-item.navbar__list-item--favorite>
<a .navbar__link-wrapper href="#">
<div .navbar__link-icon>
<i .fas .fa-2x .fa-flag-alt>
<div .navbar__link-label>_{MsgMenuLanguage}
<i .fas .fa-2x .fa-#{iconText IconFavourite}>
<div .navbar__link-label>_{MsgNavigationFavourites}
$forall n <- filter isNavHeaderPrimary nav
$case view _1 n
$of NavHeader{ navLink }
<li .navbar__list-item :highlightNav navLink:.navbar__list-item--active>
^{navWidget n}
$of NavHeaderContainer{}
<li .navbar__list-item.navbar__list-item--container-selector>
^{navWidget n}
$of _
<ul .navbar__list>
$forall n <- filter isNavHeaderSecondary $ reverse nav
$case view _1 n
$of NavHeader{ navLink }
<li .navbar__list-item :highlightNav navLink:.navbar__list-item--active>
^{navWidget n}
$of NavHeaderContainer{}
<li .navbar__list-item.navbar__list-item--container-selector>
^{navWidget n}
$of _
$forall n@(NavHeaderContainer{ navHeaderRole }, containerIdent, _, ns) <- filter isNavHeaderContainer nav
<div .navbar__container-list :navHeaderRole == NavHeaderPrimary:.navbar__container-list--left ##{containerIdent}-container>
<label .navbar__container-list-closer for=container-radio-none>
<i .fas .fa-fw .fa-#{iconText IconNavContainerClose}>
<ul>
$forall iN@(nl, _, _) <- ns
<li .navbar__container-list-item :highlightNav nl:.navbar__container-list-item--active>
^{navContainerItemWidget n iN}
$# <li .navbar__list-item--lang-wrapper uw-language-select>
$# <input type="checkbox" id="lang-checkbox" uw-no-checkbox>
$# <div id="lang-dropdown">
$# ^{langFormView'}
$# <div .navbar__list-item .navbar__list-item--language>
$# <label .navbar__link-wrapper for="lang-checkbox">
$# <div .navbar__link-icon>
$# <i .fas .fa-2x .fa-flag-alt>
$# <div .navbar__link-label>_{MsgMenuLanguage}

View File

@ -0,0 +1,4 @@
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{iconText messageIcon} :is _NotificationBroad nType:.notification--broad>
<div .notification__content>
#{messageContent}

View File

@ -2,22 +2,12 @@ $newline never
<div .pagenav>
$if hasPrimaryPageActions
<div .pagenav-prime>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes
$case menuItemType
$of PageActionPrime
<div .pagenav__list-item>
$if menuItemModal
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable>
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
$of _
$forall n@(NavPageActionPrimary{}, _, _, _) <- nav
<div .pagenav__list-item>
^{navWidget n}
$if hasSecondaryPageActions
<div .pagenav-secondary>
<div .pagenav-secondary__list>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes
$case menuItemType
$of PageActionSecondary
<div .pagenav__list-item.pagenav__list-item--secondary>
$if menuItemModal
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable>
<a .pagenav__link-wrapper.pagenav__link-wrapper--secondary href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
$of _
$forall n@(NavPageActionPrimary{}, _, _, _) <- nav
<div .pagenav__list-item.pagenav__list-item--secondary>
^{navWidget n}

View File

@ -0,0 +1,2 @@
$newline never
<a .pagenav__link-wrapper href=#{route} ##{ident}>_{SomeMessage navLabel}

View File

@ -0,0 +1,2 @@
$newline never
<a .pagenav__link-wrapper.pagenav__link-wrapper--secondary href=#{route} ##{ident}>_{SomeMessage navLabel}