Merge branch 'master' into feat/exercises

This commit is contained in:
SJost 2018-03-22 14:07:10 +01:00
commit 217ae28d9e
53 changed files with 2200 additions and 7502 deletions

View File

@ -1,21 +1,24 @@
# Quick Start Guide
Assuming Ubuntu or similar
The following Description applies to Ubuntu or similar.
## Clone repository
Clone this repository `git clone https://gitlab.cip.ifi.lmu.de/jost/UniWorX.git` and navigate into the new directory `cd UniWorX`.
## LDAP
install:<br>
`sudo apt-get install slapd ldap-utils`
install:
`sudo apt-get install slapd ldap-utils`
## PostgreSQL
install:<br>
`sudo apt-get install postgresql`
install:
`sudo apt-get install postgresql`
switch to user *postgres* (got created during installation):<br>
`sudo -i -u postgres`
switch to user *postgres* (got created during installation):
`sudo -i -u postgres`
add db user *uniworx*:<br>
`createuser --interactive`
add db user *uniworx*:
`createuser --interactive`
you'll get a prompt:
@ -24,49 +27,60 @@ Assuming Ubuntu or similar
Shall the new role be a superuser? (y/n)` - [not exactly sure. Guess not?]
```
create database *uniworx*:<br>
`createdb uniworx`
create database *uniworx*:
`createdb uniworx`
to access the database as user *uniworx* you now need to add a new linux-user called *uniworx*:<br>
`sudo adduser uniworx`
to access the database as user *uniworx* you now need to add a new linux-user called *uniworx*:
`sudo adduser uniworx`
log-in as new user *uniworx*:<br>
`sudo -i -u uniworx`
log-in as new user *uniworx*:
`sudo -i -u uniworx`
you can now use `psql uniworx` to execute SQL-commands and such.
you might for example want to add a test-account to be able to login on the page:<br>
`INSERT INTO user (plugin, ident, matrikelnummer, email, display_name) VALUES ('LDAP', '[YOUR_EMAIL_ADDRESS]', null, '[YOUR_EMAIL_ADDRESS]', '[YOUR_NAME]');`
## stack
Install with:<br>
`curl -sSL https://get.haskellstack.org/ | sh`
Install with:
`curl -sSL https://get.haskellstack.org/ | sh`
setup stack and install dependencies:<br>
`stack setup`
setup stack and install dependencies:
`stack setup`
There might be packages missing during setup. You most probably simply need to install them and try again.<br>
Instructions are easy to find via search engine of your choice and the specific error you got.<br>
Example from experience: For LDAP `ldab` and `lber` header files were missing.
During this step or the next you might get an error that says something about missing C libraries for `ldap` and `lber`. You can install these using
`sudo apt-get install libsasl2-dev libldap2-dev`
Build the app:<br>
`stack build`
If you get an error that says *You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.*
Go ahead an install `libpq-dev` with
`sudo apt-get install libpq-dev`
Run the app (with environment variable DUMMY_LOGIN set to true):<br>
`env DUMMY_LOGIN=true stack exec -- yesod devel`
Build the app:
`stack build`
`Devel application launched: http://localhost:3000`<br>
This might take a few minutes if not hours... be prepared.
install yesod:
`stack install yesod-bin --install-ghc`
## Add Dumy-Data and run the app
After building the app you can prepare the database and add some dummy data:
`./fill-db.hs`
Run the app:
`./start.sh`
`Devel application launched: http://localhost:3000`
means you are good to go.
If you followed the steps above you should now be able to login as user Gregor Kleen using `LDAP:g.kleen@ifi.lmu.de` as dummy login.
***
# Sources and more infos
PostgreSQl: <br>
PostgreSQl:
https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-16-04
stack:<br> https://docs.haskellstack.org/en/stable/README/#how-to-install
stack: https://docs.haskellstack.org/en/stable/README/#how-to-install
ldap:<br>https://wiki.ubuntuusers.de/OpenLDAP_ab_Precise/
ldap: https://wiki.ubuntuusers.de/OpenLDAP_ab_Precise/
***

View File

@ -14,6 +14,7 @@ main :: IO ()
main = db $ do
now <- liftIO getCurrentTime
let
summer2017 = TermIdentifier 2017 Summer
winter2017 = TermIdentifier 2017 Winter
summer2018 = TermIdentifier 2018 Summer
gkleen <- insert User
@ -23,6 +24,29 @@ main = db $ do
, userEmail = "G.Kleen@campus.lmu.de"
, userDisplayName = "Gregor Kleen"
}
fhamann <- insert User
{ userPlugin = "LDAP"
, userIdent = "felix.hamann@campus.lmu.de"
, userMatrikelnummer = Nothing
, userEmail = "felix.hamann@campus.lmu.de"
, userDisplayName = "Felix Hamann"
}
jost <- insert User
{ userPlugin = "LDAP"
, userIdent = "jost@tcs.ifi.lmu.de"
, userMatrikelnummer = Nothing
, userEmail = "jost@tcs.ifi.lmu.de"
, userDisplayName = "Steffen Jost"
}
void . insert $ Term
{ termName = summer2017
, termStart = fromGregorian 2017 04 09
, termEnd = fromGregorian 2017 07 14
, termHolidays = []
, termLectureStart = fromGregorian 2017 04 09
, termLectureEnd = fromGregorian 2018 07 14
, termActive = False
}
void . insert $ Term
{ termName = winter2017
, termStart = fromGregorian 2017 10 16
@ -45,9 +69,16 @@ main = db $ do
mi <- insert $ School "Institut für Mathematik" "MI"
void . insert $ UserAdmin gkleen ifi
void . insert $ UserAdmin gkleen mi
void . insert $ UserAdmin fhamann ifi
void . insert $ UserAdmin jost ifi
void . insert $ UserAdmin jost mi
void . insert $ UserLecturer gkleen ifi
void . insert $ UserLecturer fhamann ifi
void . insert $ UserLecturer jost ifi
ifiBsc <- insert $ Degree "Bachelor Informatik" ifi
ifiMsc <- insert $ Degree "Master Informatik" ifi
miBsc <- insert $ Degree "Bachelor Mathematik" mi
-- FFP
ffp <- insert Course
{ courseName = "Fortgeschrittene Funktionale Programmierung"
, courseDescription = Nothing
@ -69,3 +100,99 @@ main = db $ do
void . insert $ Lecturer gkleen ffp
void . insert $ Corrector gkleen ffp (ByProportion 1)
void . insert $ Sheet ffp "Blatt 1" Nothing NotGraded Nothing now now Nothing Nothing now now gkleen gkleen
-- EIP
eip <- insert Course
{ courseName = "Einführung in die Programmierung"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "eip"
, courseTermId = TermKey summer2017
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
void . insert $ DegreeCourse ifiBsc eip
void . insert $ DegreeCourse ifiMsc eip
void . insert $ Lecturer fhamann eip
-- interaction design
ixd <- insert Course
{ courseName = "Interaction Design (User Experience Design I & II)"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "ixd"
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = True
, courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
}
void . insert $ DegreeCourse ifiBsc ixd
void . insert $ Lecturer fhamann ixd
-- concept development
ux3 <- insert Course
{ courseName = "Concept Development (User Experience Design III)"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "ux3"
, courseTermId = TermKey winter2017
, courseSchoolId = ifi
, courseCapacity = Just 30
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
void . insert $ DegreeCourse ifiBsc ux3
void . insert $ Lecturer fhamann ux3
-- promo
pmo <- insert Course
{ courseName = "Programmierung und Modellierung"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "pmo"
, courseTermId = TermKey summer2017
, courseSchoolId = ifi
, courseCapacity = Just 50
, courseCreated = now
, courseChanged = now
, courseCreatedBy = jost
, courseChangedBy = jost
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
void . insert $ DegreeCourse ifiBsc pmo
void . insert $ Lecturer jost pmo
-- datenbanksysteme
dbs <- insert Course
{ courseName = "Datenbanksysteme"
, courseDescription = Nothing
, courseLinkExternal = Nothing
, courseShorthand = "dbs"
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 50
, courseCreated = now
, courseChanged = now
, courseCreatedBy = jost
, courseChangedBy = jost
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
void . insert $ DegreeCourse ifiBsc dbs
void . insert $ Lecturer jost dbs

View File

@ -1,5 +1,6 @@
SummerTerm year@Integer: Sommersemester #{tshow year}
WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year}
PSLimitNonPositive: “pagesize” muss größer als null sein
TermEdited tid@TermIdentifier: Semester #{termToText tid} erfolgreich editiert.
TermNewTitle: Semester editiere/anlegen.
InvalidInput: Eingaben bitte korrigieren.

View File

@ -51,6 +51,8 @@ import Handler.Utils.StudyFeatures
import System.FilePath
import Handler.Utils.Templates
-- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have
@ -82,14 +84,16 @@ type DB a = YesodDB UniWorX a
data MenuItem = MenuItem
{ menuItemLabel :: Text
, menuItemIcon :: Maybe Text
, menuItemRoute :: Route UniWorX
, menuItemAccessCallback :: Handler Bool
}
data MenuTypes
= NavbarLeft { menuItem :: MenuItem }
| NavbarRight { menuItem :: MenuItem }
| NavbarExtra { menuItem :: MenuItem }
= NavbarAside { menuItem :: MenuItem }
| NavbarRight { menuItem :: MenuItem }
| NavbarExtra { menuItem :: MenuItem }
| NavbarSecondary { menuItem :: MenuItem }
-- | A convenient synonym for creating forms.
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
@ -227,10 +231,10 @@ submissionAccess cID = do
adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool'
-> YesodDB UniWorX AuthResult
adminAccess school = do
adminAccess school = do
authId <- lift requireAuthId
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
return $ if (not $ null adrights)
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
return $ if (not $ null adrights)
then Authorized
else Unauthorized "No admin access" -- TODO internationalize
@ -274,11 +278,11 @@ isAuthorized' :: Route UniWorX -> Bool -> Handler Bool
isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite
-- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where
instance YesodBreadcrumbs UniWorX where
breadcrumb TermShowR = return ("Semester", Just HomeR)
breadcrumb TermEditR = return ("Neu", Just TermShowR)
breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR)
breadcrumb CourseListR = return ("Kurs", Just HomeR)
breadcrumb (CourseListTermR term) = return (toPathPiece term, Just TermShowR)
breadcrumb (CourseShowR term course) = return (course, Just $ CourseListTermR term)
@ -293,45 +297,63 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
breadcrumb HomeR = return ("ReWorX", Nothing)
breadcrumb (AuthR _) = return ("Login", Just HomeR)
breadcrumb ProfileR = return ("Profile", Just HomeR)
breadcrumb _ = return ("home", Nothing)
defaultLinks :: [MenuTypes]
defaultLinks = -- Define the menu items of the header.
[ NavbarLeft $ MenuItem
[ NavbarRight $ MenuItem
{ menuItemLabel = "Home"
, menuItemIcon = Just "home"
, menuItemRoute = HomeR
, menuItemAccessCallback = return True
}
, NavbarLeft $ MenuItem
{ menuItemLabel = "Kurse"
, menuItemRoute = CourseListR
, menuItemAccessCallback = return True
}
, NavbarRight $ MenuItem
{ menuItemLabel = "Users"
, menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
}
, NavbarRight $ MenuItem
{ menuItemLabel = "Profile"
, menuItemIcon = Just "profile"
, menuItemRoute = ProfileR
, menuItemAccessCallback = isJust <$> maybeAuthPair
}
, NavbarRight $ MenuItem
, NavbarSecondary $ MenuItem
{ menuItemLabel = "Login"
, menuItemIcon = Just "login"
, menuItemRoute = AuthR LoginR
, menuItemAccessCallback = isNothing <$> maybeAuthPair
}
, NavbarRight $ MenuItem
, NavbarSecondary $ MenuItem
{ menuItemLabel = "Logout"
, menuItemIcon = Just "logout"
, menuItemRoute = AuthR LogoutR
, menuItemAccessCallback = isJust <$> maybeAuthPair
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Aktuelle Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Alte Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR -- should be CourseListInactiveR or similar in the future
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Veranstaltungen"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR
, menuItemAccessCallback = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Benutzer"
, menuItemIcon = Just "user"
, menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
}
]
defaultLinkLayout :: [MenuTypes] -> Widget -> Handler Html
@ -355,12 +377,25 @@ defaultMenuLayout menu widget = do
-- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout.
let
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pc <- widgetToPageContent $ do
addStylesheet $ StaticR css_bootstrap_css
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
addScript $ StaticR js_featureChecker_js
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
$(widgetFile "default-layout")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/sortable")
$(widgetFile "standalone/inputs")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- How to run database actions.
instance YesodPersist UniWorX where
type YesodPersistBackend UniWorX = SqlBackend
@ -390,7 +425,7 @@ instance YesodAuth UniWorX where
$logDebugS "auth" $ tshow ((userPlugin, userIdent), creds)
when isDummy . (throwError =<<) . lift $
when isDummy . (throwError =<<) . lift $
maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth
let
@ -400,7 +435,7 @@ instance YesodAuth UniWorX where
userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail'
userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName'
let
newUser = User{..}
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
@ -415,13 +450,13 @@ instance YesodAuth UniWorX where
userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ]
fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures
lift $ deleteWhere [StudyFeaturesUser ==. userId]
forM_ fs $ \StudyFeatures{..} -> do
lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing
lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing
lift $ insertMany_ fs
return $ Authenticated userId
where
@ -446,7 +481,7 @@ ldapConfig _app@(appSettings -> settings) = LDAPConfig
}
where
principalName :: IsString a => a
principalName = "userPrincipalName"
principalName = "userPrincipalName"
identifierModifier _ entry = case lookup principalName $ leattrs entry of
Just [n] -> Text.pack n
_ -> error "Could not determine user principal name"

View File

@ -9,38 +9,38 @@
module Handler.Course where
import Import
import Import
import Handler.Utils
-- import Data.Time
import qualified Data.Text as T
import Data.Function ((&))
import Yesod.Form.Bootstrap3
import Yesod.Form.Bootstrap3
import Colonnade hiding (fromMaybe)
import Yesod.Colonnade
import Yesod.Colonnade
import qualified Data.UUID.Cryptographic as UUID
getCourseListR :: Handler TypedContent
getCourseListR = redirect TermShowR
getCourseListR = redirect TermShowR
getCourseListTermR :: TermId -> Handler Html
getCourseListTermR tidini = do
(term,courses) <- runDB $ (,)
(term,courses) <- runDB $ (,)
<$> get tidini
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand]
when (isNothing term) $ do
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
redirect TermShowR
-- TODO: several runDBs per TableRow are probably too inefficient!
let colonnadeTerms = mconcat
[ headed "Kürzel" $ (\ckv ->
-- TODO: several runDBs per TableRow are probably too inefficient!
let colonnadeTerms = mconcat
[ headed "Kürzel" $ (\ckv ->
let c = entityVal ckv
shd = courseShorthand c
shd = courseShorthand c
tid = courseTermId c
in [whamlet| <a href=@{CourseShowR tid shd}>#{shd} |] )
in [whamlet| <a href=@{CourseShowR tid shd}>#{shd} |] )
-- , headed "Institut" $ [shamlet| #{course} |]
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
, headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal
@ -49,52 +49,53 @@ getCourseListTermR tidini = do
partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid]
[whamlet| #{show partiNum} |]
)
, headed " " $ (\ckv ->
, headed " " $ (\ckv ->
let c = entityVal ckv
shd = courseShorthand c
shd = courseShorthand c
tid = courseTermId c
in do
adminLink <- handlerToWidget $ isAuthorized (CourseEditR tid shd ) False
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditR tid shd) else ""
[whamlet|
[whamlet|
$if adminLink == Authorized
<a href=@{CourseEditR tid shd}>
editieren
|]
)
)
]
let pageActions =
[ NavbarLeft $ MenuItem
let pageLinks =
[ NavbarAside $ MenuItem
{ menuItemLabel = "Neuer Kurs"
, menuItemIcon = Just "book"
, menuItemRoute = CourseNewR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}
]
defaultLinkLayout pageActions $ do
-- defaultLayout $ do
setTitle "Semesterkurse"
linkButton "Neuen Kurs anlegen" BCPrimary CourseNewR
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
]
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
defaultLinkLayout pageLinks $ do
setTitle "Semesterkurse"
$(widgetFile "courses")
getCourseShowR :: TermId -> Text -> Handler Html
getCourseShowR tid csh = do
mbAid <- maybeAuthId
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
dependent <- (,,)
dependent <- (,,)
<$> get (courseSchoolId course) -- join
<*> count [CourseParticipantCourseId ==. cid] -- join
<*> count [CourseParticipantCourseId ==. cid] -- join
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
Nothing -> return False
(Just aid) -> do
regL <- getBy (UniqueParticipant aid cid)
return $ isJust regL)
return $ (courseEnt,dependent)
let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
let pageActions =
[ NavbarLeft $ MenuItem
[ NavbarAside $ MenuItem
{ menuItemLabel = "Übungsblätter"
, menuItemIcon = Nothing
, menuItemRoute = SheetListR tid csh
, menuItemAccessCallback = (== Authorized) <$> isAuthorized (SheetListR tid csh) False
}
@ -102,14 +103,14 @@ getCourseShowR tid csh = do
defaultLinkLayout pageActions $ do
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course")
registerButton :: Bool -> Form ()
registerButton registered = renderAForm FormStandard $
pure () <* bootstrapSubmit regMsg
where
registerButton registered = renderAForm FormStandard $
pure () <* bootstrapSubmit regMsg
where
msg = if registered then "Abmelden" else "Anmelden"
regMsg = msg :: BootstrapSubmit Text
postCourseShowR :: TermId -> Text -> Handler Html
postCourseShowR tid csh = do
aid <- requireAuthId
@ -117,20 +118,20 @@ postCourseShowR tid csh = do
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
registered <- isJust <$> (getBy $ UniqueParticipant aid cid)
return (cid, registered)
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
case regResult of
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
case regResult of
(FormSuccess _)
| registered -> do
| registered -> do
runDB $ deleteBy $ UniqueParticipant aid cid
addMessage "info" "Sie wurden abgemeldet."
addMessage "info" "Sie wurden abgemeldet."
| otherwise -> do
actTime <- liftIO $ getCurrentTime
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
(_other) -> return () -- TODO check this!
-- redirect or not?! I guess not, since we want GET now
getCourseShowR tid csh
getCourseShowR tid csh
getCourseNewR :: Handler Html
getCourseNewR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben
@ -138,7 +139,7 @@ getCourseNewR = do
postCourseNewR :: Handler Html
postCourseNewR = courseEditHandler Nothing
getCourseEditR :: TermId -> Text -> Handler Html
getCourseEditR tid csh = do
course <- runDB $ getBy $ CourseTermShort tid csh
@ -259,28 +260,28 @@ courseEditHandler course = do
defaultLayout $ do
setTitle [shamlet| #{formTitle} |]
$(widgetFile "formPage")
data CourseForm = CourseForm
data CourseForm = CourseForm
{ cfCourseId :: Maybe CourseId -- Maybe CryptoUUIDCourse
, cfName :: Text
, cfName :: Text
, cfDesc :: Maybe Html
, cfLink :: Maybe Text
, cfShort :: Text
, cfLink :: Maybe Text
, cfShort :: Text
, cfTerm :: TermId
, cfSchool :: SchoolId
, cfCapacity :: Maybe Int
, cfCapacity :: Maybe Int
, cfHasReg :: Bool
, cfRegFrom :: Maybe UTCTime
, cfRegTo :: Maybe UTCTime
}
, cfRegFrom :: Maybe UTCTime
, cfRegTo :: Maybe UTCTime
}
instance Show CourseForm where
show cf = T.unpack (cfShort cf) ++ ' ':(show $ cfCourseId cf)
courseToForm :: Entity Course -> CourseForm
courseToForm cEntity = CourseForm
courseToForm cEntity = CourseForm
{ cfCourseId = Just $ entityKey cEntity
, cfName = courseName course
, cfDesc = courseDescription course
@ -290,26 +291,26 @@ courseToForm cEntity = CourseForm
, cfSchool = courseSchoolId course
, cfCapacity = courseCapacity course
, cfHasReg = courseHasRegistration course
, cfRegFrom = courseRegisterFrom course
, cfRegTo = courseRegisterTo course
, cfRegFrom = courseRegisterFrom course
, cfRegTo = courseRegisterTo course
}
where
course = entityVal cEntity
newCourseForm :: Maybe CourseForm -> Form CourseForm
newCourseForm template = identForm FIDcourse $ \html -> do
-- mopt hiddenField
-- mopt hiddenField
-- cidKey <- getsYesod appCryptoIDKey
-- courseId <- runMaybeT $ do
-- cid <- cfCourseId template
-- UUID.encrypt cidKey cid
-- UUID.encrypt cidKey cid
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
-- <$> pure cid -- $ join $ cfCourseId <$> template -- why doesnt this work?
<$> aopt hiddenField "KursId" (cfCourseId <$> template)
<*> areq textField (fsb "Name") (cfName <$> template)
<*> aopt htmlField (fsb "Beschreibung") (cfDesc <$> template)
<*> aopt urlField (fsb "Homepage") (cfLink <$> template)
<*> areq textField (fsb "Kürzel"
<*> areq textField (fsb "Kürzel"
-- & addAttr "disabled" "disabled"
& setTooltip "Muss innerhalb des Semesters eindeutig sein")
(cfShort <$> template)
@ -320,8 +321,8 @@ newCourseForm template = identForm FIDcourse $ \html -> do
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
<* submitButton
return $ case result of
FormSuccess courseResult
return $ case result of
FormSuccess courseResult
| errorMsgs <- validateCourse courseResult
, not $ null errorMsgs ->
(FormFailure errorMsgs,
@ -330,18 +331,18 @@ newCourseForm template = identForm FIDcourse $ \html -> do
<h4> Fehler:
<ul>
$forall errmsg <- errorMsgs
<li> #{errmsg}
<li> #{errmsg}
^{widget}
|]
)
)
_ -> (result, widget)
-- where
-- cid :: Maybe CourseId
-- cid :: Maybe CourseId
-- cid = join $ cfCourseId <$> template
validateCourse :: CourseForm -> [Text]
validateCourse (CourseForm{..}) =
validateCourse (CourseForm{..}) =
[ msg | (False, msg) <-
[
( cfRegFrom <= cfRegTo
@ -361,5 +362,3 @@ validateCourse (CourseForm{..}) =
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
)
] ]

View File

@ -9,13 +9,13 @@
module Handler.Home where
import Import
import Import
import Handler.Utils
-- import Data.Time
-- import qualified Data.Text as T
-- import Yesod.Form.Bootstrap3
-- import Yesod.Form.Bootstrap3
import Web.PathPieces (showToPathPiece, readFromPathPiece)
-- import Colonnade
@ -30,29 +30,28 @@ data CreateButton = CreateMath | CreateInf -- Dummy for Example
instance PathPiece CreateButton where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece
instance Button CreateButton where
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
label CreateInf = "Informatik"
label CreateInf = "Informatik"
cssClass CreateMath = BCInfo
cssClass CreateInf = BCPrimary
-- END Button needed here
cssClass CreateInf = BCPrimary
-- END Button needed here
getHomeR :: Handler Html
getHomeR = do
getHomeR = do
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
defaultLayout $ do
setTitle "Willkommen zum ReWorX Test!"
$(widgetFile "home")
postHomeR :: Handler Html
postHomeR :: Handler Html
postHomeR = do
((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
_other -> return ()
getHomeR
_other -> return ()
getHomeR

View File

@ -182,8 +182,9 @@ getSheetList courseEnt = do
then colBase `mappend` colAdmin
else colBase
let pageActions =
[ NavbarLeft $ MenuItem
[ NavbarAside $ MenuItem
{ menuItemLabel = "Neues Übungsblatt"
, menuItemIcon = Nothing
, menuItemRoute = SheetNewR tid csh
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}

View File

@ -9,11 +9,11 @@
module Handler.Term where
import Import
import Import
import Handler.Utils
-- import qualified Data.Text as T
import Yesod.Form.Bootstrap3
import Yesod.Form.Bootstrap3
import Colonnade hiding (bool)
import Yesod.Colonnade
@ -27,78 +27,86 @@ getTermShowR = do
-- term <- runDB $ E.select . E.from $ \(term) -> do
-- E.orderBy [E.desc $ term E.^. TermStart ]
-- return term
--
termData <- runDB $ E.select . E.from $ \term -> do
E.orderBy [E.desc $ term E.^. TermStart ]
let courseCount :: E.SqlExpr (E.Value Int)
courseCount = E.sub_select . E.from $ \course -> do
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
return E.countRows
return (term, courseCount)
--
let
termData = E.from $ \term -> do
E.orderBy [E.desc $ term E.^. TermStart ]
let courseCount :: E.SqlExpr (E.Value Int)
courseCount = E.sub_select . E.from $ \course -> do
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
return E.countRows
return (term, courseCount)
selectRep $ do
provideRep $ return $ toJSON $ map fst termData
provideRep $ do
let colonnadeTerms = mconcat
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> do
provideRep $ toJSON . map fst <$> runDB (E.select termData)
provideRep $ do
let colonnadeTerms = mconcat
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
-- Scrap this if to slow, create term edit page instead
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
[whamlet|
[whamlet|
$if adminLink == Authorized
<a href=@{TermEditExistR tid}>
#{termToText termName}
$else
$else
#{termToText termName}
|]
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
fromString $ formatTimeGerWD termLectureStart
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
fromString $ formatTimeGerWD termLectureEnd
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
bool "" tickmark termActive
|]
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ bool "" tickmark termActive
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
[whamlet|
cell [whamlet|
<a href=@{CourseListTermR tid}>
#{show numCourses} Kurse
|]
|]
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
fromString $ formatTimeGerWD termStart
stringCell $ formatTimeGerWD termStart
, headed "Semesterende" $ \(Entity _ Term{..},_) ->
fromString $ formatTimeGerWD termEnd
stringCell $ formatTimeGerWD termEnd
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
fromString $ (intercalate ", ") $ map formatTimeGerWD termHolidays
]
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
]
table <- dbTable def $ DBTable
{ dbtSQLQuery = termData
, dbtColonnade = colonnadeTerms
, dbtSorting = mempty
, dbtAttrs = tableDefault
, dbtIdent = "terms" :: Text
}
let pageActions =
[ NavbarLeft $ MenuItem
[ NavbarAside $ MenuItem
{ menuItemLabel = "Neues Semester"
, menuItemIcon = Nothing
, menuItemRoute = TermEditR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized TermEditR True
}
]
defaultLinkLayout pageActions $ do
setTitle "Freigeschaltete Semester"
encodeWidgetTable tableDefault colonnadeTerms termData
$(widgetFile "terms")
getTermEditR :: Handler Html
getTermEditR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben
termEditHandler Nothing
postTermEditR :: Handler Html
postTermEditR = termEditHandler Nothing
getTermEditExistR :: TermId -> Handler Html
getTermEditExistR tid = do
term <- runDB $ get tid
termEditHandler term
termEditHandler :: Maybe Term -> Handler Html
termEditHandler term = do
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
case result of
(FormSuccess res) -> do
-- term <- runDB $ get $ TermKey termName
-- term <- runDB $ get $ TermKey termName
runDB $ repsert (TermKey $ termName res) res
-- VOR INTERNATIONALISIERUNG:
-- let tid = termToText $ termName res
@ -113,9 +121,8 @@ termEditHandler term = do
let actionUrl = TermEditR
defaultLayout $ do
setTitle [shamlet| #{formTitle} |]
-- setTitle [whamlet| _{MsgTermNewTitle} |] -- TODO, does not work
$(widgetFile "formPage")
newTermForm :: Maybe Term -> Form Term
newTermForm template html = do
(result, widget) <- flip (renderAForm FormStandard) html $ Term
@ -127,8 +134,8 @@ newTermForm template html = do
<*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
<* submitButton
return $ case result of
FormSuccess termResult
return $ case result of
FormSuccess termResult
| errorMsgs <- validateTerm termResult
, not $ null errorMsgs ->
(FormFailure errorMsgs,
@ -137,13 +144,13 @@ newTermForm template html = do
<h4> Fehler:
<ul>
$forall errmsg <- errorMsgs
<li> #{errmsg}
<li> #{errmsg}
^{widget}
|]
)
)
_ -> (result, widget)
{-
where
set :: Text -> FieldSettings site
set = bfs
-}
{-
where
set :: Text -> FieldSettings site
set = bfs
-}

View File

@ -41,5 +41,5 @@ getUsersR = do
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
defaultLayout $ do
setTitle "Comprehensive User List"
let userList = encodeWidgetTable tableDefault colonnadeUsers users
let userList = encodeWidgetTable tableSortable colonnadeUsers users
$(widgetFile "users")

View File

@ -16,10 +16,12 @@ import Handler.Utils.DateTime as Handler.Utils
import Handler.Utils.Term as Handler.Utils
import Handler.Utils.Form as Handler.Utils
import Handler.Utils.Table as Handler.Utils
import Handler.Utils.Table.Pagination as Handler.Utils
import Handler.Utils.Zip as Handler.Utils
import Handler.Utils.Rating as Handler.Utils
import Handler.Utils.Submission as Handler.Utils
import Handler.Utils.Templates as Handler.Utils
import Text.Blaze (Markup, ToMarkup)

View File

@ -8,7 +8,7 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module Handler.Utils.Form where
module Handler.Utils.Form where
import Import
import qualified Data.Char as Char
@ -21,7 +21,7 @@ import qualified Data.Foldable as Foldable
import qualified Data.Text as T
-- import Yesod.Form.Types
import Yesod.Form.Functions (parseHelper)
import Yesod.Form.Bootstrap3
import Yesod.Form.Bootstrap3
import qualified Text.Blaze.Internal as Blaze (null)
@ -53,7 +53,7 @@ data FormLayout = FormStandard
renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do
(res, (($ []) -> views)) <- aFormToForm aform
let widget = $(widgetFile "form")
let widget = $(widgetFile "widgets/form")
return (res, widget)
----------------------------
@ -72,12 +72,12 @@ class (Enum a, Bounded a, Ord a, PathPiece a) => Button a where
cssClass :: a -> ButtonCssClass
cssClass _ = BCDefault
data BtnDelete = BtnDelete | BtnAbort
deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance PathPiece BtnDelete where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece
@ -85,7 +85,7 @@ instance PathPiece BtnDelete where -- for displaying the button only, not rea
instance Button BtnDelete where
label BtnDelete = "Löschen"
label BtnAbort = "Abrechen"
cssClass BtnDelete = BCDanger
cssClass BtnAbort = BCDefault
@ -101,45 +101,45 @@ instance Button SubmitButton where
label BtnSubmit = "Submit"
cssClass BtnSubmit = BCPrimary
-- -- Looks like a button, but is just a link (e.g. for create course, etc.)
-- -- Looks like a button, but is just a link (e.g. for create course, etc.)
-- data LinkButton = LinkButton (Route UniWorX)
-- deriving (Enum, Eq, Ord, Bounded, Read, Show)
--
--
-- instance PathPiece LinkButton where
-- LinkButton route = ???
linkButton :: Widget -> ButtonCssClass -> Route UniWorX -> Widget
linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=button>^{lbl} |]
-- [whamlet|
-- <form method=post action=@{url}>
-- <form method=post action=@{url}>
-- <input type="hidden" name="_formid" value="identify-linkButton">
-- <button .btn .#{bcc2txt cls} type=submit value="Link to @{url}">^{lbl}
-- |]
-- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}>
-- |]
-- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}>
buttonField :: Button a => a -> Field Handler a
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
where
where
fieldEnctype = UrlEncoded
fieldView fid name attrs _val _ =
fieldView fid name attrs _val _ =
[whamlet|
<button .btn .#{bcc2txt $ cssClass btn} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn}
|]
fieldParse [] _ = return $ Right Nothing
fieldParse [str] _
fieldParse [str] _
| str == toPathPiece btn = return $ Right $ Just btn
| otherwise = return $ Left "Wrong button value"
fieldParse _ _ = return $ Left "Multiple button values"
combinedButtonField :: Button a => [a] -> AForm Handler [Maybe a]
combinedButtonField btns = traverse b2f btns
where
b2f b = aopt (buttonField b) "" Nothing
b2f b = aopt (buttonField b) "" Nothing
submitButton :: AForm Handler ()
submitButton = void $ combinedButtonField [BtnSubmit]
@ -148,10 +148,10 @@ submitButton = void $ combinedButtonField [BtnSubmit]
combinedButtonField :: Button a => [a] -> Form m -> Form (a,m)
combinedButtonField btns inner csrf = do
buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
(innerRes,innerWdgt) <- inner
let widget = do
let widget = do
[whamlet|
#{csrf}
^{innerWdgt}
@ -173,14 +173,14 @@ combinedButtonField btns inner csrf = do
accResult' _ x@(FormSuccess _) = x --SJ: Is this safe? Shouldn't Failure override Success?
accResult' (FormSuccess Nothing) x = x
accResult' FormMissing _ = FormMissing
accResult' (FormFailure errs) _ = FormFailure errs
accResult' (FormFailure errs) _ = FormFailure errs
-}
-- buttonForm :: Button a => Markup -> MForm (HandlerT UniWorX IO) (FormResult a, (WidgetT UniWorX IO ()))
buttonForm :: (Button a) => Form a
buttonForm csrf = do
buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
let widget = do
[whamlet|
@ -202,7 +202,7 @@ buttonForm csrf = do
accResult' (FormFailure errs) _ = FormFailure errs
------------
-- Fields --
------------
@ -260,42 +260,42 @@ utcTimeField = Field
|]
, fieldEnctype = UrlEncoded
}
where
where
fieldTimeFormat :: String
--fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%eT%H:%M"
readTime :: Text -> Either FormMessage UTCTime
readTime t =
readTime t =
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
(Just time) -> Right time
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t
showTime :: UTCTime -> Text
showTime :: UTCTime -> Text
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
fsb :: Text -> FieldSettings site
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
fsb :: Text -> FieldSettings site
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
where
where
newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,valu)]
newAttrs (p@(a,v):t)
newAttrs (p@(a,v):t)
| attr==a = (a,T.append valu $ cons ' ' v):t
| otherwise = p:(newAttrs t)
| otherwise = p:(newAttrs t)
addAttrs :: Text -> [Text] -> FieldSettings site -> FieldSettings site
addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
where
where
newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,T.intercalate " " valus)]
newAttrs (p@(a,v):t)
newAttrs (p@(a,v):t)
| attr==a = (a,T.intercalate " " (v:valus)):t
| otherwise = p:(newAttrs t)
| otherwise = p:(newAttrs t)
addClass :: Text -> FieldSettings site -> FieldSettings site
addClass = addAttr "class"
@ -312,7 +312,7 @@ addIdClass :: Text -> Text -> FieldSettings site -> FieldSettings site
addIdClass gId gClass fs = fs { fsId= Just gId, fsAttrs=("class",gClass):(fsAttrs fs) }
setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated
setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated
setClass fs c = fs { fsAttrs=("class",c):(fsAttrs fs) }
setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated
@ -322,4 +322,3 @@ setTooltip :: String -> FieldSettings site -> FieldSettings site
setTooltip tt fs
| null tt = fs { fsTooltip = Nothing }
| otherwise = fs { fsTooltip = Just $ fromString tt }

View File

@ -23,12 +23,15 @@ import Data.Either
-- Table design
tableDefault :: Attribute
tableDefault = customAttribute "class" "table table-striped table-hover"
tableDefault = customAttribute "class" "table table-striped table-hover"
tableSortable :: Attribute
tableSortable = customAttribute "class" "js-sortable"
-- Colonnade Tools
numberColonnade :: (IsString c) => Colonnade Headed Int c
numberColonnade = headed "Nr" (fromString.show)
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
@ -39,8 +42,8 @@ encodeHeadedWidgetTableNumbered attrs colo tdata =
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
where
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
numberCol = headed "Nr" (fromString.show.fst)
numberCol = headed "Nr" (fromString.show.fst)
headedRowSelector :: ( PathPiece b
, Eq b
)
@ -74,7 +77,7 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
selectionIdent <- newFormIdent
(selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing
let
selColonnade :: Colonnade Headed Int (Cell UniWorX)
selColonnade = headed "Markiert" $ cell . fvInput . (selectionBoxes !!)

View File

@ -0,0 +1,121 @@
{-# LANGUAGE NoImplicitPrelude
, ExistentialQuantification
, RecordWildCards
, OverloadedStrings
, TemplateHaskell
, LambdaCase
, ViewPatterns
#-}
module Handler.Utils.Table.Pagination where
import Import
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect)
import Text.Blaze (Attribute)
import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_)
import Data.Map (Map)
import Colonnade hiding (bool, fromMaybe)
import Yesod.Colonnade
import Text.Hamlet (hamletFile)
data SortColumn = forall a. PersistField a => SortColumn { getSortColumn :: E.SqlExpr (E.Value a) }
data SortDirection = SortAsc | SortDesc
deriving (Eq, Ord, Enum, Show, Read)
sqlSortDirection :: (SortColumn, SortDirection) -> E.SqlExpr E.OrderBy
sqlSortDirection (SortColumn e, SortAsc ) = E.asc e
sqlSortDirection (SortColumn e, SortDesc) = E.desc e
data DBTable = forall a r h i.
( Headedness h
, E.SqlSelect a r
, PathPiece i
) => DBTable
{ dbtSQLQuery :: E.SqlQuery a
, dbtColonnade :: Colonnade h r (Cell UniWorX)
, dbtSorting :: Map Text SortColumn
, dbtAttrs :: Attribute
, dbtIdent :: i
}
data PaginationSettings = PaginationSettings
{ psSorting :: [(SortColumn, SortDirection)]
, psLimit :: Int64
, psPage :: Int64
, psShortcircuit :: Bool
}
instance Default PaginationSettings where
def = PaginationSettings
{ psSorting = []
, psLimit = 50
, psPage = 0
, psShortcircuit = False
}
newtype PSValidator = PSValidator { runPSValidator :: Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) }
instance Default PSValidator where
def = PSValidator $ \case
Nothing -> def
Just ps -> swap . (\act -> execRWS act () ps) $ do
l <- gets psLimit
when (l <= 0) $ do
modify $ \ps -> ps { psLimit = psLimit def }
tell . pure $ SomeMessage MsgPSLimitNonPositive
dbTable :: PSValidator -> DBTable -> Handler Widget
dbTable PSValidator{..} DBTable{ dbtIdent = (toPathPiece -> dbtIdent), .. } = do
let
sortingOptions = mkOptionList
[ Option t' (c, d) t'
| (t, c) <- mapToList dbtSorting
, d <- [SortAsc, SortDesc]
, let t' = t <> "-" <> tshow d
]
sortingField = Field parse (\_ _ _ _ _ -> return ()) UrlEncoded
where
parse optlist _ = case mapM (olReadExternal sortingOptions) optlist of
Nothing -> return $ Left "Error parsing values"
Just res -> return $ Right $ Just res
(_, defPS) = runPSValidator Nothing
wIdent n
| not $ null dbtIdent = dbtIdent <> "-" <> n
| otherwise = n
psResult <- runInputGetResult $ PaginationSettings
<$> ireq sortingField (wIdent "sorting")
<*> (fromMaybe (psLimit defPS) <$> iopt intField (wIdent "pagesize"))
<*> (fromMaybe (psPage defPS) <$> iopt intField (wIdent "page"))
<*> ireq checkBoxField (wIdent "table-only")
$(logDebug) . tshow $ (,,,) <$> (length . psSorting <$> psResult)
<*> (psLimit <$> psResult)
<*> (psPage <$> psResult)
<*> (psShortcircuit <$> psResult)
let
(errs, PaginationSettings{..}) = case psResult of
FormSuccess ps -> runPSValidator $ Just ps
FormFailure errs -> first (map SomeMessage errs <>) $ runPSValidator Nothing
FormMissing -> runPSValidator Nothing
sqlQuery' = dbtSQLQuery
<* E.orderBy (map sqlSortDirection psSorting)
<* E.limit psLimit
<* E.offset (psPage * psLimit)
mapM_ (addMessageI "warning") errs
rows <- runDB $ E.select sqlQuery'
bool return (sendResponse <=< tblLayout) psShortcircuit $ do
encodeCellTable dbtAttrs dbtColonnade rows
where
tblLayout :: Widget -> Handler Html
tblLayout = widgetToPageContent >=> (\tbl -> withUrlRenderer $(hamletFile "templates/table-layout.hamlet"))

View File

@ -0,0 +1,8 @@
{-# LANGUAGE NoImplicitPrelude, TemplateHaskell #-}
module Handler.Utils.Templates where
import Import.NoFoundation
lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum")

View File

@ -16,3 +16,6 @@ import Data.Fixed as Import
import CryptoID as Import
import Data.UUID as Import (UUID)
import Text.Lucius as Import

7022
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

9
static/css/fonts.css Normal file
View File

@ -0,0 +1,9 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('../fonts/glyphicons-halflings-regular.woff') format('woff'),
url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}

34
static/css/icons.css Normal file
View File

@ -0,0 +1,34 @@
.glyphicon {
position: relative;
display: inline-block;
width: 40px;
height: 40px;
}
.glyphicon::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: 'Glyphicons Halflings';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon--home::before {
content: '\e021';
}
.glyphicon--book::before {
content: '\e043';
}
.glyphicon--profile::before {
content: '\e019';
}
.glyphicon--user::before {
content: '\e008';
}
.glyphicon--login::before {
content: '\e161';
}
.glyphicon--logout::before {
content: '\e163';
}

View File

@ -0,0 +1,7 @@
window.addEventListener('touchstart', function onFirstTouch() {
// we could use a class
document.body.classList.add('touch-supported');
// we only need to know once that a human touched the screen, so we can stop listening now
window.removeEventListener('touchstart', onFirstTouch, false);
}, false);

View File

@ -1,38 +1,34 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
#{courseName course}
$maybe school <- schoolMB
<h4>
#{schoolName school}
<div .course-header>
<div .course-header__info>
<table>
<tr>
<th>Teilnehmer
<td>
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<tr>
<th>Anmeldezeitraum
<td>
$maybe regFrom <- courseRegisterFrom course
#{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<div>
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<div .course-header__title>
<h1>#{courseName course}
$maybe school <- schoolMB
<h4>#{schoolName school}
<div .container>
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
$maybe descr <- courseDescription course
<h2 #description>Beschreibung
<p> #{descr}
$maybe link <- courseLinkExternal course
<h4 #linl>Homepage
<a href=#{link}>#{link}
<div .row>
<div .col-lg-12>
<h4>Teilnehmer
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<br>
$maybe regFrom <- courseRegisterFrom course
Anmeldezeitraum: #{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<hr>
$maybe descr <- courseDescription course
<h2 #description>Beschreibung
<p> #{descr}
$maybe link <- courseLinkExternal course
<h4 #linl>Homepage
<a href=#{link}>#{link}

19
templates/course.lucius Normal file
View File

@ -0,0 +1,19 @@
.course-header {
/*display: flex;
flex-direction: row;
justify-content: space-between;*/
}
.course-header__title {
align-self: baseline;
}
.course-header__info {
border: 1px solid var(--greybase);
padding: 13px;
align-self: center;
float: right;
}
.course-header__info table {
margin: 0;
}

4
templates/courses.hamlet Normal file
View File

@ -0,0 +1,4 @@
<div .container>
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
<div .scrolltable>
^{coursesTable}

View File

@ -19,12 +19,7 @@ $newline never
\<!--[if lt IE 9]>
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
\<![endif]-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js">
\<!-- Bootstrap-3.3.7 compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">
<script>
/* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */
/* AJAX requests should add that token to a header to be validated by the server. */
@ -43,8 +38,6 @@ $newline never
\ });
}
<script>
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/,'js');
<body>
^{pageBody pc}

View File

@ -1,60 +1,17 @@
<!-- navigation -->
^{navbar}
<!-- Static navbar -->
<nav .navbar.navbar-default.navbar-static-top>
<div .container>
<div .navbar-header>
<button type="button" .navbar-toggle.collapsed data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<div .main>
<div #navbar .collapse.navbar-collapse>
<ul .nav.navbar-nav>
$forall menuType <- menuTypes
$case menuType
$of NavbarLeft (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of NavbarExtra (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<!-- secondary navigation at the side -->
^{asidenav}
<ul .nav.navbar-nav.navbar-right>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<!-- Page Contents -->
<div .container>
$if not $ Just HomeR == mcurrentRoute
<ul .breadcrumb>
$forall bc <- parents
<li>
<a href="@{fst bc}">#{snd bc}
<li .active>#{title}
<div .main__content>
<!-- alerts -->
$forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">#{msg}
$if (Just HomeR == mcurrentRoute)
<!-- actual content -->
^{widget}
$else
<div .container>
<div .row>
<div .col-md-12>
^{widget}
<!-- Footer -->
<footer .footer>
<div .container>
<p .text-muted>
#{appCopyright $ appSettings master}

View File

@ -1,75 +1,211 @@
.masthead,
.navbar {
background-color: rgb(27, 28, 29);
:root {
/* THEME 1 */
--base00: #72a85b;
--base-bg-color: #1d1c1d;
--base-font-color: #fff;
--sec-font-color: #fff;
--box-bg-color: #3c3c3c;
/* THEME 2 */
--base00: #38428a;
--base-bg-color: #ffffff;
--base-font-color: rgb(53, 53, 53);
--sec-font-color: #eaf2ff;
--box-bg-color: #dddddd;
/* THEME 3 */
--darkbase: #364B60;
--lightbase: #2490E8;
--lighterbase: #60C2FF;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--fontbase: #34303a;
--fontsec: #5b5861;
/* THEME 4 */
--darkbase: #263C4C;
--lightbase: #598EB5;
--lighterbase: #5F98C2;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--blackbase: #1A2A36;
--fontbase: #34303a;
--fontsec: #5b5861;
--primarybase: #4C7A9C;
/* THEME INDEPENDENT COLORS */
--errorbase: red;
--warningbase: #fe7700;
--validbase: #2dcc35;
--infobase: var(--darkbase);
/* FONTS */
--fontfamilybase: "Source Sans Pro", Helvetica, sans-serif;
/* DIMENSIONS */
--header-height: 80px;
--header-height-collapsed: 50px;
}
.navbar-default .navbar-nav > .active > a {
background-color: transparent;
border-bottom: 2px solid white;
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.navbar-nav {
padding-bottom: 1em;
body {
background-color: white;
color: var(--fontbase);
font-family: var(--fontfamilybase);
font-weight: 400;
font-size: 16px;
overflow-y: scroll;
}
.masthead {
margin-top: -21px;
color: white;
text-align: center;
min-height: 500px;
a,
a:visited {
text-decoration: none;
font-weight: 600;
transition: color .2s ease, background-color .2s ease;
}
.masthead .header {
max-width: 700px;
margin: 0 auto;
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
ul {
list-style-type: none;
}
.masthead h1.header {
margin-top: 1em;
margin-bottom: 0em;
font-size: 4.5em;
line-height: 1.2em;
font-weight: normal;
}
.masthead h2 {
font-size: 1.7em;
font-weight: normal;
.list--inline > li {
display: inline-block;
}
.masthead .btn {
margin: 1em 0;
h1, h2, h3, h4, h5 {
font-weight: 600;
}
h1 {
font-size: 32px;
margin: 20px 0 10px;
}
h2 {
font-size: 24px;
margin: 10px 0 5px;
}
h3 {
font-size: 20px;
margin: 5px 0;
}
h4 {
font-size: 16px;
margin: 0;
}
table {
margin: 21px 0;
}
/* Common styles for all types */
.bs-callout {
padding: 20px;
margin: 20px 0;
border: 1px solid #eee;
border-left-width: 5px;
border-radius: 3px;
.scrolltable {
width: 100%;
overflow: auto;
}
.bs-callout p:last-child {
margin-bottom: 0;
th, td {
text-align: left;
padding: 0 13px 0 7px;
vertical-align: baseline;
}
th:first-child,
td:first-child {
padding-left: 0;
border-left: 0;
}
th {
border-left: 2px solid var(--greybase);
}
/* LAYOUT */
.main {
display: flex;
min-height: calc(100vh - var(--header-height));
overflow: hidden;
}
.bs-callout-info {
border-left-color: #1b809e;
.main__content {
position: relative;
background-color: white;
padding: 0 40px;
flex: 1;
z-index: 0;
overflow: hidden;
> .container {
margin: 20px 0;
}
p {
margin: 10px 0;
}
a {
color: var(--darkbase);
}
a:hover {
color: var(--lightbase);
}
}
/* Space things out */
.bs-docs-section {
margin-bottom: 60px;
}
.bs-docs-section:last-child {
margin-bottom: 0;
.pseudo-focus {
outline: 5px auto var(--lightbase);
outline: 5px auto -webkit-focus-ring-color;
}
#message {
margin-bottom: 40px;
/* GENERAL BUTTON STYLES */
input[type="submit"],
input[type="button"],
button,
.btn {
outline: 0;
border: 0;
box-shadow: 0;
background-color: var(--lightbase);
color: white;
padding: 10px 17px;
min-width: 100px;
transition: all .1s;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
display: inline-block;
}
input.btn-primary,
button.btn-primary,
.btn.btn-primary {
background-color: var(--primarybase);
}
input.btn-info,
button.btn-info,
.btn.btn-info {
background-color: var(--infobase)
}
input[type="submit"][disabled],
input[type="button"][disabled],
button[disabled],
.btn[disabled] {
opacity: 0.3;
background-color: var(--greybase);
cursor: default;
}
input[type="submit"]:not([disabled]):hover,
input[type="button"]:not([disabled]):hover,
button:not([disabled]):hover,
.btn:not([disabled]):hover {
background-color: var(--lighterbase);
text-decoration: underline;
}
input[type="submit"].btn-info:hover,
input[type="button"].btn-info:hover,
button.btn-info:hover,
.btn.btn-info:hover {
background-color: var(--greybase)
}
.alert-debug {

View File

@ -1,9 +0,0 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div :fvRequired view:.required :not $ fvRequired view:.optional :isJust $ fvErrors view:.has-error>
$if not (Blaze.null $ fvLabel view)
<label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -9,4 +9,4 @@
<div .col-md-10 .col-lg-9>
<div .bs-callout bs-callout-info well>
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
^{formWidget}
^{formWidget}

View File

@ -1,82 +1,69 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
ReWorX - Demo
<h3>
Testumgebung für die Re-Implementierung von
<a href="https://uniworx.ifi.lmu.de/">
UniWorX
<div .container>
<!-- Starting
================================================== -->
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h2 #start>Übersicht
<p>
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
<div .alert .alert-danger>
Das System ist noch nicht produktiv
einsetzbar
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h2 #design>Design
<p>
Wir konzentrieren uns derzeit
ausschließlich auf die Funktionalität.
<p>
Insbesondere Formulare zeigen
alle Eingabefelder und Knöpfe
ohne eine gezielte Anordnung
und Reihenfolge.
Dies läßt sich leicht nachträglich einstellen.
<p>
Momentan werden noch keine speziellen Grafiken oder CSS verwendet;
sondern nur gewöhnliches Bootstrap3.
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h3 #design>Teilweise funktionierende Abschnitte
<ul .list-group>
<h1>ReWorX - Demo
<h3>
Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<p>
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
<li .list-group-item>
<a href=@{UsersR}>Benutzer Verwaltung
<p .alert .alert-danger>Das System ist noch nicht produktiv einsetzbar
<li .list-group-item>
<a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<hr>
<div .container>
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
<li .list-group-item>
<ul>
<li .list-group-item>
<a href=@{UsersR}>Benutzer Verwaltung
<li .list-group-item>
<a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen
editieren und anzeigen
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<h3 #design>Funktionen zum Testen
<ul .list-group>
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<li .list-group-item>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}
<li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen
editieren und anzeigen
<hr>
<div .container>
<h2 .js-show-hide__toggle data-collapsed=true>Tabellen
<table .js-sortable>
<thead>
<tr>
<th .sorted-asc>ID
<th>TH1
<th>TH2
<th>TH3
<tbody>
<tr>
<td>0
<td>14
<td>CON2
<td>3
<tr>
<td>1
<td>5
<td>ONT2
<td>13
<tr>
<td>2
<td>CONT1
<td>NT2
<td>43
<tr>
<td>3
<td>43
<td>T2C2
<td>35
<hr>
<div .container>
<h2>Funktionen zum Testen
<ul>
<li>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}

0
templates/home.julius Normal file
View File

View File

@ -0,0 +1 @@
<!-- only here to be able to include inputs using `toWidget` -->

View File

@ -0,0 +1,177 @@
(function() {
'use strict';
window.utils = window.utils || {};
// makes <label> smaller if <input> is focussed
window.utils.reactiveInputLabel = function(input, label) {
// updates to dom
if (input.value.length > 0) {
label.classList.add('reactive-label--small');
}
// add event listeners
input.addEventListener('focus', function() {
label.classList.add('reactive-label--small');
});
label.addEventListener('click', function() {
label.classList.add('reactive-label--small');
input.focus();
});
input.addEventListener('blur', function() {
if (input.value.length < 1) {
label.classList.remove('reactive-label--small');
}
});
};
window.utils.reactiveFileUpload = function(input, parent) {
var currValidInputCount = 0;
var addMore = false;
var inputName = input.getAttribute('name');
// FileInput PseudoClass
function FileInput(container, input, label, remover) {
this.container = container;
this.input = input;
this.label = label;
this.remover = remover;
addListener(this);
this.addTo = function(parentElement) {
parentElement.appendChild(this.container);
}
this.remove = function() {
this.container.remove();
}
this.isValid = function() {
return this.container.classList.contains('file-input__container--valid');
}
}
function addNextInput() {
var inputs = parent.querySelectorAll('.file-input__container');
if (inputs[inputs.length - 1].classList.contains('file-input__container--valid')) {
makeInput(inputName).addTo(parent);
}
}
// updates submitbutton and form-group-stripe
function updateForm() {
var submitBtn = parent.parentElement.querySelector('[type=submit]');
parent.classList.remove('form-group--has-error');
if (currValidInputCount > 0) {
if (parent.classList.contains('form-group')) {
parent.classList.add('form-group--valid')
}
submitBtn.removeAttribute('disabled');
addNextInput();
} else {
if (parent.classList.contains('form-group')) {
parent.classList.remove('form-group--valid')
}
submitBtn.setAttribute('disabled', 'disabled');
}
}
// addseventlistener destInput
function addListener(fileInput) {
fileInput.input.addEventListener('change', function(event) {
if (fileInput.input.value.length > 0) {
// update label
var filePath = fileInput.input.value.replace(/\\/g, '/').split('/');
var fileName = filePath[filePath.length - 1];
fileInput.label.innerHTML = fileName;
// increase count if this field was empty previously
if (!fileInput.isValid()) {
currValidInputCount++;
}
fileInput.container.classList.add('file-input__container--valid')
// show next input
} else {
currValidInputCount--;
fileInput.remove();
}
updateForm();
});
fileInput.input.addEventListener('focus', function() {
fileInput.container.classList.add('pseudo-focus');
});
fileInput.input.addEventListener('blur', function() {
fileInput.container.classList.remove('pseudo-focus');
});
fileInput.label.addEventListener('click', function() {
fileInput.input.click();
});
fileInput.remover.addEventListener('click', function() {
if (fileInput.isValid()) {
currValidInputCount--;
}
fileInput.remove();
updateForm();
});
}
// create new wrapped input element with name name
function makeInput(name) {
var cont = document.createElement('div');
var desc = document.createElement('span');
var nextInput = document.createElement('input');
var remover = document.createElement('div');
cont.classList.add('file-input__container');
desc.classList.add('file-input__label', 'btn');
remover.classList.add('file-input__remover');
nextInput.setAttribute('name', name);
nextInput.setAttribute('type', 'file');
cont.appendChild(nextInput);
cont.appendChild(desc);
cont.appendChild(remover);
return new FileInput(cont, nextInput, desc, remover);
}
// initial setup
function setup() {
var newInput = makeInput(inputName);
input.remove();
newInput.addTo(parent);
updateForm();
}
setup();
}
window.utils.reactiveFormGroup = function(formGroup, input) {
// updates to dom
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
input.addEventListener('input', function() {
formGroup.classList.remove('form-group--has-error');
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
// setup reactive labels
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
var input = document.querySelector('#' + label.getAttribute('for'));
var parent = label.parentElement;
var type = input.getAttribute('type');
var isFileInput = /file/i.test(type);
var isListening = !RegExp(['date', 'checkbox', 'radio', 'hidden', 'file'].join('|')).test(type);
var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required');
if (isInFormGroup) {
window.utils.reactiveFormGroup(parent, input);
}
if (isFileInput) {
window.utils.reactiveFileUpload(input, parent);
}
if (isListening) {
window.utils.reactiveInputLabel(input, label);
} else {
label.classList.remove('reactive-label');
}
});
});

View File

@ -0,0 +1,306 @@
/* GENERAL STYLES FOR FORMS */
form {
margin: 20px 0;
}
/* TEXT INPUTS */
input[type="text"],
input[type="password"],
input[type="url"],
input[type="number"],
input[type="email"] {
background-color: rgba(0, 0, 0, 0.05);
padding: 7px 3px 7px;
outline: 0;
border: 0;
border-bottom: 2px solid var(--darkbase);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
color: var(--fontbase);
transition: all .1s;
font-size: 16px;
min-width: 300px;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
input[type="email"]:focus {
border-bottom-color: var(--lightbase);
background-color: transparent;
}
/* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */
textarea {
outline: 0;
border: 0;
padding: 7px 4px;
min-width: 300px;
min-height: 100px;
font-family: var(--fontfamilybase);
font-size: 16px;
color: var(--fontbase);
background-color: rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
border-bottom: 2px solid var(--darkbase);
}
textarea:focus {
background-color: transparent;
border-bottom-color: var(--lightbase);
}
/* FORM GROUPS */
.form-group {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(150px, max-content));
grid-auto-columns: minmax(150px, max-content);
grid-gap: 5px;
align-items: center;
margin: 10px 0;
padding-left: 10px;
border-left: 8px solid transparent;
}
.form-group--required {
border-left: 8px solid var(--lighterbase);
}
.form-group--valid {
border-left: 8px solid var(--validbase);
}
.form-group--has-error {
border-left: 8px solid var(--errorbase) !important;
}
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
input[type="checkbox"] {
position: relative;
height: 20px;
width: 20px;
-webkit-appearance: none;
cursor: pointer;
}
input[type="checkbox"]::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background-color: var(--lighterbase);
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
}
input[type="checkbox"]:checked::before {
background-color: var(--lightbase);
}
input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
/* CUSTOM CHECKBOXES AND RADIO BOXES */
/* Completely replaces legacy checkbox and radiobox */
.checkbox,
.radio {
position: relative;
margin: 3px;
> [type="checkbox"],
> [type="radio"] {
display: none;
}
> label {
display: block;
padding: 7px 13px 7px 30px;
background-color: var(--darkbase);
border-radius: 4px;
color: white;
cursor: pointer;
}
> label::before,
> label::after {
content: '';
position: absolute;
top: 15px;
left: 4px;
display: block;
width: 20px;
height: 20px;
background-color: white;
transition: all .2s;
}
> label::before {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
}
> label::after {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
}
> :checked + label {
background-color: var(--lightbase);
text-decoration: underline;
}
&:hover > label::before,
> :checked + label::before {
transform: scale(1, 1) rotate(45deg);
}
&:hover > label::after,
> :checked + label::after {
transform: scale(1, 1) rotate(-45deg);
}
}
.radio > label::before {
transform: scale(0.01, 0.01) rotate(45deg);
}
.radio > label::after {
transform: scale(0.01, 0.01) rotate(-45deg);
}
.radio::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
border: 2px solid white;
z-index: -1;
}
/* REACTIVE LABELS */
.reactive-label {
cursor: text;
color: var(--fontsec);
transform: translate(0, 0);
transition: all .1s;
}
.reactive-label--small {
cursor: default;
color: var(--fontbase);
}
@media (max-width: 999px) {
.form-group {
grid-template-rows: 30px;
grid-template-columns: 1fr;
align-items: baseline;
margin-top: 17px;
}
.reactive-label {
position: relative;
transform: translate(2px, 30px);
}
.reactive-label--small {
transform: translate(2px, 0px);
color: var(--fontsec);
/*font-size: 14px;*/
}
}
/* CUSTOM FILE INPUT */
input[type="file"] {
color: white;
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
outline: 0;
border: 0;
}
.file-input__container {
grid-column-start: 2;
display: flex;
justify-content: space-between;
}
.file-input__label,
.file-input__remover {
display: block;
border-radius: 2px;
padding: 5px 13px;
color: var(--whitebase);
cursor: pointer;
}
.file-input__label {
text-align: left;
position: relative;
height: 30px;
}
.file-input__label.btn {
padding: 5px 13px;
}
.file-input__label::after,
.file-input__label::before {
position: absolute;
content: '';
background-color: white;
width: 16px;
height: 2px;
top: 14px;
top: 50%;
left: 12px;
left: 50%;
}
.file-input__label::after {
transform: translate(-50%, -50%) rotate(90deg);
}
.file-input__label::before {
transform: translate(-50%, -50%);
}
.file-input__remover {
display: none;
width: 40px;
height: 30px;
text-align: center;
background-color: var(--warningbase);
position: relative;
margin-left: 10px;
}
.file-input__remover::before {
position: absolute;
content: '';
width: 16px;
height: 2px;
top: 14px;
left: 12px;
background-color: white;
}
.file-input__container--valid > .file-input__label {
background-color: var(--lightbase);
}
.file-input__container--valid > .file-input__label::before,
.file-input__container--valid > .file-input__label::after {
content: none;
}
.file-input__container--valid > .file-input__remover {
display: block;
}
@media (max-width: 999px) {
.file-input__container {
grid-column-start: 1;
}
}

View File

@ -0,0 +1 @@
<!-- only here to be able to include showHide using `toWidget` -->

View File

@ -0,0 +1,59 @@
/**
* div.js-show-hide
* div.js-show-hide-toggle
* toggle here
* div
* content here
*/
document.addEventListener('DOMContentLoaded', function() {
var elements = Array.from(document.querySelectorAll('.js-show-hide__toggle')),
toggles = [],
path = new URL(window.location.href).pathname;
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var toggle = toggles[el.dataset.index];
toggle.collapsed = !toggle.collapsed;
toggle.parent.classList.toggle('js-show-hide--collapsed', toggle.collapsed);
updateLocalStorage();
});
}
function updateLocalStorage(id) {
var lsData = getStateFromLocalStorage();
lsData[path] = toggles.map(t => {
return {id: t.index, collapsed: t.collapsed};
});
window.localStorage.setItem('showHidesToggles', JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(id, fallBack) {
var lsData = getStateFromLocalStorage();
if (lsData[path] && lsData[path][id] && lsData[path][id].id === id) {
return lsData[path][id].collapsed;
}
return fallBack;
}
function getStateFromLocalStorage() {
return JSON.parse(window.localStorage.getItem('showHidesToggles')) || {};
}
elements.forEach(function(el, i) {
el.dataset.index = i;
var coll = collapsedStateInLocalStorage(i, el.dataset.collapsed === 'true');
if (coll) {
el.parentElement.classList.add('js-show-hide--collapsed')
}
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
toggles.push({index: i, collapsed: coll, parent: el.parentElement});
addEventHandler(el);
});
});

View File

@ -0,0 +1,49 @@
.js-show-hide {
position: relative;
}
.js-show-hide__toggle {
position: relative;
}
.js-show-hide__toggle:hover {
cursor: pointer;
}
.js-show-hide__toggle::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-right: 8px solid transparent;
border-bottom: 8px solid transparent;
}
.js-show-hide__toggle::before,
.js-show-hide--collapsed .js-show-hide__toggle:hover::before {
left: -28px;
top: 10px;
border-left: 8px solid transparent;
border-top: 8px solid var(--lightbase);
}
.js-show-hide__toggle:hover::before,
.js-show-hide--collapsed .js-show-hide__toggle::before {
border-left: 8px solid var(--lightbase);
border-top: 8px solid transparent;
top: 5px;
left: -22px;
}
.js-show-hide__target {
transition: all .2s ease;
overflow: hidden;
}
.js-show-hide--collapsed > .js-show-hide__target {
display: block;
height: 0;
margin: 0;
padding: 0;
max-height: 0;
}

View File

@ -0,0 +1 @@
<!-- only here to be able to include sortable using `toWidget` -->

View File

@ -0,0 +1,88 @@
/**
* delcare a table as sortable by adding class 'js-sortable'
*/
document.addEventListener('DOMContentLoaded', function() {
var tables = [];
var ASC = 1;
var DESC = -1;
function initTable(table, tableIndex) {
var trs = table.querySelectorAll('tr');
var ths = table.querySelectorAll('th');
var trContents = [];
Array.from(trs).forEach(function(tr, rowIndex) {
if (rowIndex === 0) {
// register table headers as sort-listener
Array.from(tr.querySelectorAll('th')).forEach(function(th, thIndex) {
th.addEventListener('click', function(el) {
sortTableBy(tableIndex, thIndex);
});
});
} else {
// register table rows
trContents.push(Array.from(tr.querySelectorAll('td')).map(function(td) {
return td.innerHTML;
}));
}
});
tables.push({
el: table,
ths: ths,
sortBy: 0,
sortDir: ASC,
trContents,
});
}
function updateThs(tableIndex, thIndex, sortOrder) {
Array.from(tables[tableIndex].ths).forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
});
var suffix = sortOrder > 0 ? 'asc' : 'desc';
tables[tableIndex].ths[thIndex].classList.add('sorted-' + suffix);
}
function sortTableBy(tableIndex, thIndex) {
var table = tables[tableIndex];
var sortKey = thIndex;
var sortOrder = ASC;
if (table.sortBy === sortKey) {
sortOrder = table.sortDir === ASC ? DESC : ASC;
}
table.trContents.sort(dynamicSort(sortKey, sortOrder));
tables[tableIndex].sortBy = thIndex;
tables[tableIndex].sortDir = sortOrder;
updateThs(tableIndex, thIndex, sortOrder);
Array.from(table.el.querySelectorAll('tr')).forEach(function(tr, trIndex) {
if (trIndex > 0) {
Array.from(tr.querySelectorAll('td')).forEach(function (td, tdIndex) {
td.innerHTML = table.trContents[trIndex - 1][tdIndex];
});
}
});
}
function dynamicSort(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
if ((isNaN(aVal) && !isNaN(bVal)) || (!isNaN(aVal) && isNaN(bVal))) {
console.error('trying to sort table by row with mixed content: "%s", "%s"', a[key], b[key]);
}
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var result = (aVal < bVal) ? -1 : (aVal > bVal) ? 1 : 0;
return result * order;
}
}
var rawTables = document.querySelectorAll('.js-sortable');
Array.from(rawTables).forEach(function(table, i) {
initTable(table, i);
});
});

View File

@ -0,0 +1,31 @@
table.js-sortable th {
cursor: pointer;
position: relative;
padding-right: 20px;
}
table.js-sortable th.sorted-asc,
table.js-sortable th.sorted-desc {
color: var(--darkbase);
}
table.js-sortable th.sorted-asc::after,
table.js-sortable th.sorted-desc::after {
content: '';
position: absolute;
right: 0;
top: 15px;
width: 0;
height: 0;
transform: translateY(-100%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
table.js-sortable th.sorted-asc::after {
border-top: 8px solid var(--lightbase);
}
table.js-sortable th.sorted-desc::after {
border-bottom: 8px solid var(--lightbase);
}

View File

@ -1,6 +1,8 @@
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<div .container>
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
^{subTable}
<button .btn .btn-default type=submit >Markierte herunterladen
<div .container>
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
^{subTable}
<button .btn .btn-default type=submit >Markierte herunterladen

View File

@ -0,0 +1 @@
^{pageBody tbl}

5
templates/terms.hamlet Normal file
View File

@ -0,0 +1,5 @@
<div .container>
<h1>Semesterübersicht
<div .scrolltable>
^{table}

View File

@ -0,0 +1,32 @@
<aside .main__aside>
<div .asidenav>
<div .asidenav__box--dont-hide>
<ul .asidenav__list>
$forall menuType <- menuTypes
$case menuType
$of NavbarAside (MenuItem label mIcon route _)
<li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{route}>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<div .asidenav__link-label>#{label}
$of _
<div .asidenav__box--dont-hide>
<h3 .asidenav__box-title.js-show-hide__toggle>
WiSe 17/18
<ul .asidenav__list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">
<div .asidenav__link-triple>IXD
<div .asidenav__link-label>Interaction Design
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/show">
<div .asidenav__link-triple>FFP
<div .asidenav__link-label>Fortgeschrittene Funktionale Programmierung
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/dbs/show">
<div .asidenav__link-triple>DBS
<div .asidenav__link-label>Datenbanksysteme
<div .asidenav__toggler>

View File

@ -0,0 +1,81 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.aside = function(asideEl, topNav) {
var collapsed = false;
var collClass = 'main__aside--collapsed';
var animClass = 'main__aside--transitioning';
var aboveCollapsedNav = false;
init();
function init() {
var collLS = window.localStorage.getItem('asidenavCollapsed') === 'true';
if (document.body.getBoundingClientRect().width < 999 || collLS) {
asideEl.classList.add(collClass);
collapsed = true;
if (topNav) {
topNav.style.paddingLeft = '90px';
window.setTimeout(function() {
topNav.classList.add('navbar--animated');
}, 200);
}
} else if (topNav) {
topNav.classList.add('navbar--animated');
}
}
function check() {
if (collapsed && !hasClass() || !collapsed && hasClass()) {
asideEl.classList.add(animClass);
asideEl.classList.toggle(collClass, collapsed);
if (topNav) {
topNav.style.paddingLeft = collapsed ? '90px' : '';
}
window.localStorage.setItem('asidenavCollapsed', collapsed);
}
}
function hasClass() {
return asideEl.classList.contains(collClass);
}
asideEl.querySelector('.asidenav__toggler').addEventListener('click', function(event) {
collapsed = !collapsed;
check();
}, false);
asideEl.addEventListener('transitionend', function(event) {
if (event.propertyName === 'opacity') {
asideEl.classList.remove(animClass);
}
}, false);
window.addEventListener('resize', function() {
collapsed = document.body.getBoundingClientRect().width < 999;
check();
}, false);
asideEl.addEventListener('mouseover', function(event) {
if (!collapsed) {
return false;
}
aboveCollapsedNav = true;
console.log(event);
window.setTimeout(function() {
if (aboveCollapsedNav && !document.body.classList.contains('touch-supported')) {
asideEl.classList.add('pseudo-hover');
}
}, 430);
}, false);
asideEl.addEventListener('mouseleave', function(event) {
aboveCollapsedNav = false;
asideEl.classList.remove('pseudo-hover');
}, false);
};
})();
document.addEventListener('DOMContentLoaded', function() {
utils.aside(document.querySelector('.main__aside'), document.querySelector('.navbar'));
});

View File

@ -0,0 +1,163 @@
.main__aside {
position: relative;
background-color: var(--darkbase);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1;
flex: 0 0 300px;
overflow: hidden;
}
.main__aside--transitioning {
transition: flex-basis .2s ease;
}
.main__aside--transitioning .asidenav__box{
transition: opacity .2s ease;
}
.main__aside--collapsed.pseudo-hover {
overflow: visible;
}
.main__aside--collapsed {
width: 50px;
flex-basis: 50px;
.asidenav__box-title {
width: 50px;
padding: 0;
}
}
.main__aside--collapsed .asidenav__box {
opacity: 0;
}
.main__aside--collapsed .asidenav__box--dont-hide {
opacity: 1;
}
.main__aside--collapsed:not(.main__aside--transitioning) .asidenav__box {
height: 0;
padding: 0;
margin: 0;
visibility: hidden;
}
.asidenav {
width: 300px;
margin-top: 20px;
color: white;
.js-show-hide__toggle::before {
top: 14px;
right: 12px;
left: auto !important;
}
.js-show-hide__toggle:hover::before,
.js-show-hide--collapsed .js-show-hide__toggle::before {
top: 10px !important;
right: 8px !important;
}
.js-show-hide--collapsed .js-show-hide__toggle:hover::before {
top: 14px !important;
right: 12px !important;
}
}
.asidenav__box {
margin: 10px 0;
padding: 10px 0;
width: 100%;
border-bottom: 4px solid var(--whitebase);
background-color: var(--darkbase);
}
.asidenav__box-title {
padding: 7px 13px;
a {
color: white;
}
}
.asidenav__list-item {
position: relative;
background-color: white;
color: var(--darkbase);
margin: 4px 0;
&:not(.asidenav__list-item--active):hover {
color: white;
background-color: var(--darkbase);
.asidenav__link-wrapper,
.asidenav__link-label {
color: white;
}
.asidenav__link-triple {
transform: scale(1.2, 1);
}
}
}
.asidenav__list-item--active {
background-color: var(--darkbase);
color: white;
.asidenav__link-wrapper {
pointer-events: none;
color: white;
}
}
.asidenav__link-wrapper {
position: relative;
display: flex;
height: 50px;
align-items: center;
justify-content: flex-start;
color: var(--darkbase);
z-index: 1;
.glyphicon {
width: 50px;
}
.asidenav__link-triple {
background-color: var(--darkbase);
color: var(--whitebase);
height: 50px;
width: 50px;
display: inline-block;
line-height: 50px;
text-align: center;
margin-right: 13px;
flex-shrink: 0;
outline: 1px solid white;
}
}
.asidenav__toggler {
position: absolute;
bottom: 20px;
height: 50px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color .2s ease;
border-top: 1px solid var(--whitebase);
border-bottom: 1px solid var(--whitebase);
cursor: pointer;
&::before {
content: '\e079';
display: block;
font-family: 'Glyphicons Halflings';
color: var(--whitebase);
}
&:hover {
background-color: var(--lightbase);
}
}
.main__aside--collapsed .asidenav__toggler::before {
content: '\e080';
}

View File

@ -0,0 +1,7 @@
<div .breadcrumbs__container>
<ul .breadcrumbs__list.list--inline>
$forall bc <- parents
<li .breadcrumbs__item>
<a .breadcrumbs__link href="@{fst bc}">#{snd bc}
&gt;
<li .breadcrumbs__item--active>#{title}

View File

@ -0,0 +1,14 @@
.breadcrumbs__container {
position: relative;
color: white;
z-index: 10;
align-self: flex-end;
margin-bottom: 20px;
transition: margin-bottom .2s ease;
}
.breadcrumbs__container--animated {
transition: left .2s ease;
}
.breadcrumbs__container .breadcrumbs__link {
color: white;
}

View File

@ -0,0 +1,9 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
$if not (Blaze.null $ fvLabel view)
<label .form-group__label .reactive-label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -0,0 +1,51 @@
(function() {
'use strict';
window.utils = window.utils || {};
// registers input-listener for each element in <elements> (array) and
// enables <button> if <fn> for these elements returns true
window.utils.reactiveButton = function(elements, button, fn) {
if (elements.length == 0) {
return false;
}
var checkboxes = elements[0].getAttribute('type') === 'checkbox';
var eventType = checkboxes ? 'change' : 'input';
updateButtonState();
elements.forEach(function(el) {
el.addEventListener(eventType, function() {
updateButtonState();
});
});
function updateButtonState() {
if (fn.call(null, elements)) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
// auto reactiveButton submit-buttons with required fields
var forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
var requireds = form.querySelectorAll('[required]');
var submitBtn = form.querySelector('[type=submit]');
if (submitBtn && requireds) {
window.utils.reactiveButton(Array.from(requireds), submitBtn, function(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
});
}
});
});

View File

View File

@ -0,0 +1,10 @@
<div>
<h2>A little lorem ipsum to make the page scrollable
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.

View File

@ -0,0 +1,25 @@
<div .navbar-container>
<nav .navbar.js-sticky-navbar>
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
^{breadcrumbs}
<ul .navbar__list.list--inline>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label mIcon route _)
<li .navbar__list-item :Just route == mcurrentRoute:.navbar__list-item--active>
<a .navbar__link-wrapper href=@{route}>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<div .navbar__link-label>#{label}
$of NavbarSecondary (MenuItem label mIcon route _)
<li .navbar__list-item.navbar__list-item--secondary :Just route == mcurrentRoute:.navbar__list-item--active>
<a .navbar__link-wrapper href=@{route}>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<div .navbar__link-label>#{label}
$of _
<div .navbar__pushdown>

View File

@ -0,0 +1,28 @@
/**
* .js-sticky-navbar
* ul
* li Item 1
* li Item 2
*/
document.addEventListener('DOMContentLoaded', function() {
var ticking = false;
var nav = document.querySelector('.js-sticky-navbar');
window.addEventListener('scroll', function(e) {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
ticking = true;
}
}, false);
// checks scroll direction and shows/hides navbar accordingly
function checkScroll() {
var sticky = window.scrollY > 0;
nav.classList.toggle('navbar--sticky', sticky);
ticking = false;
}
checkScroll();
});

View File

@ -0,0 +1,127 @@
.navbar {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: var(--header-height);
padding-right: 5vw;
padding-left: 340px;
background: var(--darkbase); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--darkbase) 0%, #425d79 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(bottom, var(--darkbase) 0%,#425d79 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to top, var(--darkbase) 0%,#425d79 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
color: white;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
top: 0;
left: 0;
overflow: hidden;
transition: height 0.2s ease;
}
.navbar__list {
align-self: flex-end;
white-space: nowrap;
}
.navbar__list-item {
position: relative;
transition: background-color .1s ease;
.glyphicon {
position: relative;
width: 100%;
height: 20px;
}
.glyphicon::before {
height: 20px;
}
}
.navbar :last-child {
margin-left: auto;
}
.navbar .navbar__link-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80px;
color: var(--whitebase);
transition: height .2s ease;
}
.navbar__link-label {
transition: opacity .2s ease;
padding: 0 13px;
color: white;
text-transform: uppercase;
}
.navbar__list-item--secondary {
margin-left: 20px;
color: var(--greybase);
}
.navbar__list-item--secondary + .navbar__list-item--secondary {
margin-left: 0;
border-left: 0;
}
.navbar__list-item--active {
background-color: white;
color: var(--darkbase);
.navbar__link-wrapper {
color: var(--darkbase);
}
}
.navbar__list-item--active .navbar__link-wrapper {
pointer-events: none;
}
.navbar__list-item--active .navbar__link-label {
color: var(--darkbase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover {
background-color: var(--darkbase);
color: var(--whitebase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper {
color: var(--whitebase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-label {
color: var(--whitebase);
}
.navbar__list-item--secondary .navbar__link-wrapper,
.navbar__list-item--secondary .navbar__link-label {
color: var(--greybase);
}
.navbar--sticky {
height: var(--header-height-collapsed);
z-index: 100;
.navbar__link-wrapper {
height: 50px;
}
.breadcrumbs__container {
margin-bottom: 7px;
}
}
.navbar--animated {
transition: all .2s ease;
}
.navbar__pushdown {
/*display: none;*/
height: var(--header-height);
transition: height .2s ease;
}
.navbar--sticky + .navbar__pushdown {
display: block;
height: var(--header-height-collapsed);
}