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

View File

@ -1,48 +1,89 @@
import { Utility } from '../../core/utility'; import { Utility } from '../../core/utility';
import './navbar.sass'; import './navbar.sass';
import * as throttle from 'lodash.throttle';
export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper';
export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]'; const HEADER_CONTAINER_INITIALIZED_CLASS = '.navbar-header-container--initialized';
const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized';
@Utility({ @Utility({
selector: LANGUAGE_SELECT_UTIL_SELECTOR, selector: HEADER_CONTAINER_UTIL_SELECTOR,
}) })
export class LanguageSelectUtil { export class NavHeaderContainerUtil {
_element; _element;
checkbox; radioButton;
closeButton;
container;
wasOpen;
_throttleUpdateWasOpen;
constructor(element) { constructor(element) {
if (!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; return false;
} }
this._element = element; 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) { start() {
if (!this._element.contains(event.target) && window.document.contains(event.target)) { window.addEventListener('click', this.clickHandler.bind(this));
this.checkbox.checked = false; 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() { close() {
// TODO 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 = [ export const NavbarUtils = [
LanguageSelectUtil, NavHeaderContainerUtil,
]; ];

View File

@ -21,28 +21,115 @@
.navbar .navbar
position: fixed position: fixed
display: flex
flex-direction: row
align-items: center
justify-content: flex-start
right: 0 right: 0
top: 0 top: 0
left: var(--asidenav-width-xl) left: var(--asidenav-width-xl)
height: var(--header-height) min-height: var(--header-height)
background-color: var(--color-primary) background-color: var(--color-primary)
color: white color: white
z-index: 20 z-index: 20
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2) box-shadow: 0 0 4px rgba(0, 0, 0, 0.2)
overflow: auto overflow: auto
transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1) transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
margin: 0
padding: 10px 0
@media (max-width: 1199px) @media (max-width: 1199px)
.navbar
left: var(--asidenav-width-lg) left: var(--asidenav-width-lg)
@media (max-width: 768px) @media (max-width: 768px), (max-height: 500px)
.navbar
left: 0 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 // links
.navbar__link-wrapper .navbar__link-wrapper
@ -67,12 +154,13 @@
padding: 2px 4px padding: 2px 4px
text-transform: uppercase text-transform: uppercase
font-weight: 600 font-weight: 600
font-size: 16px
@media (min-width: 769px) @media (min-width: 769px) and (min-height: 501px)
.navbar__link-wrapper .navbar__link-wrapper
border: 1px solid rgba(255, 255, 255, 0.7) border: 1px solid rgba(255, 255, 255, 0.7)
@media (max-width: 768px) @media (max-width: 768px), (max-height: 500px)
.navbar__link-wrapper .navbar__link-wrapper
box-shadow: none box-shadow: none
min-width: 0 min-width: 0
@ -86,22 +174,42 @@
transform: scale(0.65) transform: scale(0.65)
margin-bottom: 0 margin-bottom: 0
// navbar list .navbar__container-link
.navbar__list display: block
white-space: nowrap
@media (min-width: 769px) and (min-height: 501px)
border: 1px solid rgba(255, 255, 255, 0.7)
text-decoration: none
+ .navbar__list @media (max-width: 768px), (max-height: 500px)
margin-left: 12px text-decoration: underline
@media (min-width: 769px) height: 30px
.navbar__list:last-of-type color: var(--color-lightwhite) !important
padding-right: 40px 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 // list item
.navbar__list-item .navbar__list-item
@ -124,34 +232,10 @@
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper
margin-left: 0 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 // "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) @media (min-width: 426px)
.logged-in .navbar__list-item--favorite
.navbar__list display: none !important
.navbar__list-item--favorite
display: none !important
.navbar__list-item--active .navbar__list-item--active
background-color: var(--color-lightwhite) background-color: var(--color-lightwhite)
@ -163,7 +247,7 @@
.navbar__list-item--active .navbar__link-wrapper .navbar__list-item--active .navbar__link-wrapper
color: var(--color-dark) 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) background-color: var(--color-dark)
color: var(--color-lightwhite) color: var(--color-lightwhite)
@ -186,43 +270,5 @@
display: block display: block
height: var(--header-height-collapsed) height: var(--header-height-collapsed)
@media (max-width: 768px) .navbar__container-radio--none, .navbar__container-radio
.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
display: none 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. CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert.
Users: Benutzer Users: Benutzer
HomeHeading: Aktuelle Termine NewsHeading: Aktuelles
LoginHeading: Authentifizierung LoginHeading: Authentifizierung
LoginTitle: Authentifizierung LoginTitle: Authentifizierung
ProfileHeading: Benutzereinstellungen ProfileHeading: Benutzereinstellungen
@ -508,9 +508,9 @@ NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen f
TokensLastReset: Tokens zuletzt invalidiert TokensLastReset: Tokens zuletzt invalidiert
TokensResetSuccess: Authorisierungs-Tokens invalidiert TokensResetSuccess: Authorisierungs-Tokens invalidiert
HomeOpenAllocations: Offene Zentralanmeldungen NewsOpenAllocations: Offene Zentralanmeldungen
HomeUpcomingSheets: Anstehende Übungsblätter NewsUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Prüfungen NewsUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"} NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
CloseAlert: Schliessen CloseAlert: Schliessen
@ -1114,7 +1114,7 @@ InvalidRoute: Konnte URL nicht interpretieren
MenuOpenCourses: Kurse mit offener Registrierung MenuOpenCourses: Kurse mit offener Registrierung
MenuOpenAllocations: Aktive Zentralanmeldungen MenuOpenAllocations: Aktive Zentralanmeldungen
MenuHome: Aktuell MenuNews: Aktuell
MenuInformation: Informationen MenuInformation: Informationen
MenuLegal: Rechtliche Informationen MenuLegal: Rechtliche Informationen
MenuDataProt: Datenschutzerklärung MenuDataProt: Datenschutzerklärung
@ -1561,7 +1561,6 @@ ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusPoints': Umrechnung von Übungspunkten
ExamBonusManual': Manuelle Berechnung 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 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. CorrectorsDefaulted: List of correctors was automatically generated based on those of preceding sheets for this course. No data has been saved, yet.
Users: Users Users: Users
HomeHeading: Home NewsHeading: News
LoginHeading: Authentication LoginHeading: Authentication
LoginTitle: Authentication LoginTitle: Authentication
ProfileHeading: Settings ProfileHeading: Settings
@ -506,9 +506,9 @@ NotificationSettingsHeading displayName: Notification settings for #{displayName
TokensLastReset: Tokens last reset TokensLastReset: Tokens last reset
TokensResetSuccess: Successfully invalidated all authorisation tokens TokensResetSuccess: Successfully invalidated all authorisation tokens
HomeOpenAllocations: Active central allocations NewsOpenAllocations: Active central allocations
HomeUpcomingSheets: Upcoming exercise sheets NewsUpcomingSheets: Upcoming exercise sheets
HomeUpcomingExams: Upcoming exams NewsUpcomingExams: Upcoming exams
NumCourses num: #{num} #{pluralEN num "course" "courses"} NumCourses num: #{num} #{pluralEN num "course" "courses"}
CloseAlert: Close CloseAlert: Close
@ -1113,7 +1113,7 @@ InvalidRoute: Could not interpret url
MenuOpenCourses: Courses with open registration MenuOpenCourses: Courses with open registration
MenuOpenAllocations: Active central allocations MenuOpenAllocations: Active central allocations
MenuHome: Home MenuNews: News
MenuInformation: Information MenuInformation: Information
MenuLegal: Legal MenuLegal: Legal
MenuDataProt: Data protection MenuDataProt: Data protection
@ -1559,7 +1559,6 @@ ExamBonusRule: Bonus points from exercises
ExamNoBonus': No automatic exam bonus ExamNoBonus': No automatic exam bonus
ExamBonusPoints': Compute from exercise achievements ExamBonusPoints': Compute from exercise achievements
ExamBonusManual': Manual computation 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 ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room

2
routes
View File

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

View File

@ -101,7 +101,7 @@ import Data.List (cycle)
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.) -- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Home import Handler.News
import Handler.Info import Handler.Info
import Handler.Help import Handler.Help
import Handler.Profile 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 | ("en" : _) <- lang' = mr MsgEnglish
| otherwise = lang | otherwise = lang
where where
mr = renderMessage foundation ls mr = renderMessage foundation $ lang : filter (/= lang) ls
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>) embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel 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 siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do
setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading 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") $(widgetFile "exam-users")

View File

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

View File

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

View File

@ -80,7 +80,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
return res return res
invitationSuccessMsg _ _ = invitationSuccessMsg _ _ =
return $ SomeMessage MsgExamOfficeUserInvitationAccepted return $ SomeMessage MsgExamOfficeUserInvitationAccepted
invitationUltDest _ _ = return $ SomeRoute HomeR invitationUltDest _ _ = return $ SomeRoute NewsR
makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId)) 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 import Import
@ -9,21 +9,25 @@ import Database.Esqueleto.Utils.TH
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
getHomeR :: Handler Html getNewsR :: Handler Html
getHomeR = do getNewsR = do
muid <- maybeAuthId muid <- maybeAuthId
defaultLayout $ do defaultLayout $ do
setTitleI MsgHomeHeading setTitleI MsgNewsHeading
when (is _Nothing muid) $
notificationWidget NotificationBroad Info $(i18nWidgetFile "pitch")
case muid of case muid of
Just uid -> do Just uid -> do
homeUpcomingExams uid newsUpcomingExams uid
homeUpcomingSheets uid newsUpcomingSheets uid
Nothing -> Nothing ->
$(i18nWidgetFile "unauth-home") $(i18nWidgetFile "unauth-news")
homeUpcomingSheets :: UserId -> Widget newsUpcomingSheets :: UserId -> Widget
homeUpcomingSheets uid = do newsUpcomingSheets uid = do
cTime <- liftIO getCurrentTime cTime <- liftIO getCurrentTime
let tableData :: E.LeftOuterJoin let tableData :: E.LeftOuterJoin
(E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant)) (E.SqlExpr (Entity Course))) (E.SqlExpr (Entity Sheet))) (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 , dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing , dbtCsvDecode = Nothing
} }
$(widgetFile "home/upcomingSheets") $(widgetFile "news/upcomingSheets")
homeUpcomingExams :: UserId -> Widget newsUpcomingExams :: UserId -> Widget
homeUpcomingExams uid = do newsUpcomingExams uid = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do ((Any hasExams, examTable), warningDays) <- liftHandler . runDB $ do
User {userWarningDays} <- get404 uid User {userWarningDays} <- get404 uid
@ -255,6 +259,6 @@ homeUpcomingExams uid = do
(, userWarningDays) <$> dbTable examDBTableValidator examDBTable (, userWarningDays) <$> dbTable examDBTableValidator examDBTable
$(widgetFile "home/upcomingExams") $(widgetFile "news/upcomingExams")

View File

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

View File

@ -399,7 +399,7 @@ invitationR' InvitationConfig{..} = liftHandler $ do
Nothing -> do Nothing -> do
addMessageI Info MsgInvitationDeclined addMessageI Info MsgInvitationDeclined
deleteBy . UniqueInvitation itEmail $ invRef @junction fid deleteBy . UniqueInvitation itEmail $ invRef @junction fid
return . Just $ SomeRoute HomeR return . Just $ SomeRoute NewsR
Just (jData, formCtx) -> do Just (jData, formCtx) -> do
let junction = review _InvitableJunction (invitee, fid, jData) let junction = review _InvitableJunction (invitee, fid, jData)
mResult <- invitationInsertHook itEmail fEnt iData junction formCtx $ insertUniqueEntity junction 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 Model.Tokens as Import
import Utils.Tokens as Import import Utils.Tokens as Import
import Utils.Frontend.Modal as Import import Utils.Frontend.Modal as Import
import Utils.Frontend.Notification as Import
import Utils.Lens as Import import Utils.Lens as Import
import Settings as Import import Settings as Import

View File

@ -46,6 +46,7 @@ import Data.Scientific
import Data.Time.Clock (NominalDiffTime, nominalDay) import Data.Time.Clock (NominalDiffTime, nominalDay)
import Utils import Utils
import Utils.Frontend.Notification
-- import Utils.Message -- import Utils.Message
-- import Utils.PathPiece -- import Utils.PathPiece
-- import Utils.Route -- import Utils.Route
@ -869,26 +870,15 @@ wformMessage :: (MonadHandler m) => Message -> WForm m ()
wformMessage = void . aFormToWForm . aformMessage wformMessage = void . aFormToWForm . aformMessage
formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site) formMessage :: (MonadHandler m) => Message -> MForm m (FormResult (), FieldView site)
formMessage Message{..} = do formMessage msg = do
return (FormSuccess (), FieldView return (FormSuccess (), FieldView
{ fvLabel = mempty { fvLabel = mempty
, fvTooltip = Nothing , fvTooltip = Nothing
, fvId = idFormMessageNoinput , fvId = idFormMessageNoinput
, fvErrors = Nothing , fvErrors = Nothing
, fvRequired = False , fvRequired = False
, fvInput = [whamlet| , fvInput = notification NotificationNarrow msg
$newline never
<div .notification .notification-#{toPathPiece messageStatus} .fa-#{maybe defaultIcon iconText messageIcon}>
<div .notification__content>
#{messageContent}
|]
}) })
where
defaultIcon = case messageStatus of
Success -> "check-circle"
Info -> "info-circle"
Warning -> "exclamation-circle"
Error -> "exclamation-triangle"
--------------------- ---------------------
-- Form evaluation -- -- 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 | IconApplicationVeto
| IconApplicationFiles | IconApplicationFiles
| IconTooltipDefault | 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 :: Icon -> Text
iconText = \case iconText = \case
IconNew -> "seedling" IconNew -> "seedling"
IconOK -> "check" IconOK -> "check"
IconNotOK -> "times" IconNotOK -> "times"
IconWarning -> "exclamation" IconWarning -> "exclamation"
IconProblem -> "bolt" IconProblem -> "bolt"
IconVisible -> "eye" IconVisible -> "eye"
IconInvisible -> "eye-slash" IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap" IconCourse -> "graduation-cap"
IconEnrolTrue -> "user-plus" IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash" IconEnrolFalse -> "user-slash"
IconPlanned -> "cog" IconPlanned -> "cog"
IconAnnounce -> "bullhorn" IconAnnounce -> "bullhorn"
IconExam -> "poll-h" IconExam -> "poll-h"
IconExamRegisterTrue -> "calendar-check" IconExamRegisterTrue -> "calendar-check"
IconExamRegisterFalse -> "calendar-times" IconExamRegisterFalse -> "calendar-times"
IconCommentTrue -> "comment-alt" IconCommentTrue -> "comment-alt"
IconCommentFalse -> "comment-slash" -- comment-alt-slash is not available for free IconCommentFalse -> "comment-alt-slash"
IconLink -> "link" IconLink -> "link"
IconFileDownload -> "file-download" IconFileDownload -> "file-download"
IconFileUpload -> "file-upload" IconFileUpload -> "file-upload"
IconFileZip -> "file-archive" IconFileZip -> "file-archive"
IconFileCSV -> "file-csv" IconFileCSV -> "file-csv"
IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar) IconSFTQuestion -> "question-circle" -- for SheetFileType only, should all be round (similar)
IconSFTHint -> "life-ring" -- for SheetFileType only IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope" IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt" IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt" IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash" IconApplyFalse -> "trash"
IconNoCorrectors -> "user-slash" IconNoCorrectors -> "user-slash"
IconApplicationVeto -> "times" IconApplicationVeto -> "times"
IconApplicationFiles -> "file-alt" IconApplicationFiles -> "file-alt"
IconTooltipDefault -> "question-circle" 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 Universe Icon
instance Finite Icon instance Finite Icon

View File

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

View File

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

View File

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

View File

@ -1,5 +1,11 @@
$newline never $newline never
$if not isModal $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 --> <!-- secondary navigation at the side -->
^{asidenav} ^{asidenav}
<!-- navigation --> <!-- navigation -->
@ -15,7 +21,7 @@ $if not isModal
$if not isModal $if not isModal
<!-- breadcrumbs --> <!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute $if not $ Just NewsR == mcurrentRoute
^{breadcrumbsWgt} ^{breadcrumbsWgt}
<div .main__content-body> <div .main__content-body>

View File

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

View File

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

View File

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

View File

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

View File

@ -28,12 +28,9 @@ $newline never
<div .asidenav__link-label>#{courseName} <div .asidenav__link-label>#{courseName}
<div .asidenav__nested-list-wrapper> <div .asidenav__nested-list-wrapper>
<ul .asidenav__nested-list.list--iconless> <ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions $forall (NavLink{navLabel}, route) <- pageActions
$case menuItemType <li .asidenav__nested-list-item>
$of PageActionPrime <a .asidenav__link-wrapper href=#{route}>_{navLabel}
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _
<div .asidenav__sigillum> <div .asidenav__sigillum>
<img src=@{StaticR img_lmu_sigillum_svg}> <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 $newline never
<footer .footer> <footer .footer>
<div .footer-links> <div .footer-links>
$forall (MenuItem{menuItemType, menuItemRoute = _, menuItemIcon = _, menuItemLabel, menuItemModal = _}, menuIdent, route) <- menuTypes $forall (NavFooter{ navLink }, navIdent, navRoute', _) <- filter isNavFooter nav
$case menuItemType <a href=#{navRoute'} ##{navIdent}>
$of Footer _{navLink}
$# 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 _

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 $newline never
<a .navbar__link-wrapper href=#{route} ##{menuIdent}> <a .navbar__link-wrapper href=#{route} ##{ident}>
$if isJust menuItemIcon <div .navbar__link-icon>
<div .navbar__link-icon> <i .fas .fa-2x .fa-#{iconText navIcon}>
<i .fas .fa-2x .fa-#{fromMaybe "none" menuItemIcon}> <div .navbar__link-label>_{SomeMessage navLabel}
<div .navbar__link-label>_{SomeMessage menuItemLabel}

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 $newline never
<div .navbar-container> <div .navbar-container>
<div .navbar-shadow> <div .navbar-shadow>
<nav .navbar.js-sticky-navbar> <nav .navbar>
<div .navbar__list-wrapper>
<ul .navbar__list.list--inline.navbar__list-left> <ul .navbar__list.navbar__list-left>
$# manually add favorites to navbar for small screens $# manually add favorites to navbar for small screens
<li .navbar__list-item.navbar__list-item--favorite> <li .navbar__list-item.navbar__list-item--favorite>
<a .navbar__link-wrapper href="#"> <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">
<div .navbar__link-icon> <div .navbar__link-icon>
<i .fas .fa-2x .fa-flag-alt> <i .fas .fa-2x .fa-#{iconText IconFavourite}>
<div .navbar__link-label>_{MsgMenuLanguage} <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> <div .pagenav>
$if hasPrimaryPageActions $if hasPrimaryPageActions
<div .pagenav-prime> <div .pagenav-prime>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes $forall n@(NavPageActionPrimary{}, _, _, _) <- nav
$case menuItemType <div .pagenav__list-item>
$of PageActionPrime ^{navWidget n}
<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 _
$if hasSecondaryPageActions $if hasSecondaryPageActions
<div .pagenav-secondary> <div .pagenav-secondary>
<div .pagenav-secondary__list> <div .pagenav-secondary__list>
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes $forall n@(NavPageActionPrimary{}, _, _, _) <- nav
$case menuItemType <div .pagenav__list-item.pagenav__list-item--secondary>
$of PageActionSecondary ^{navWidget n}
<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 _

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}