Merge branch 'initial_thoughts_on_frontend' into 'master'
bootstrap-less frontend Closes #29 and #18 See merge request !10
This commit is contained in:
commit
303f2163b0
@ -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
|
||||
@ -80,6 +82,7 @@ mkYesodData "UniWorX" $(parseRoutesFile "routes")
|
||||
|
||||
data MenuItem = MenuItem
|
||||
{ menuItemLabel :: Text
|
||||
, menuItemIcon :: Maybe Text
|
||||
, menuItemRoute :: Route UniWorX
|
||||
, menuItemAccessCallback :: Handler Bool
|
||||
}
|
||||
@ -88,6 +91,7 @@ data MenuTypes
|
||||
= NavbarLeft { 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)
|
||||
@ -219,10 +223,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"
|
||||
|
||||
@ -230,7 +234,7 @@ lecturerAccess :: Maybe SchoolId
|
||||
-> YesodDB UniWorX AuthResult
|
||||
lecturerAccess school = do
|
||||
authId <- lift requireAuthId
|
||||
lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) []
|
||||
lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) []
|
||||
return $ if (not $ null lecrights)
|
||||
then Authorized
|
||||
else Unauthorized "No lecturer access"
|
||||
@ -250,15 +254,15 @@ 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)
|
||||
breadcrumb CourseEditR = return ("Neu", Just CourseListR)
|
||||
breadcrumb CourseEditR = return ("Neu", Just CourseListR)
|
||||
breadcrumb (CourseEditExistR _ _) = return ("Editieren", Just CourseListR)
|
||||
|
||||
breadcrumb (SheetListR tid csh) = return ("Kurs", Just $ CourseShowR tid csh)
|
||||
@ -266,42 +270,48 @@ 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"
|
||||
, menuItemIcon = Just "book"
|
||||
, menuItemRoute = CourseListR
|
||||
, menuItemAccessCallback = return True
|
||||
}
|
||||
, NavbarRight $ MenuItem
|
||||
}
|
||||
, NavbarLeft $ MenuItem
|
||||
{ menuItemLabel = "Users"
|
||||
, menuItemIcon = Just "user"
|
||||
, menuItemRoute = UsersR
|
||||
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
|
||||
}
|
||||
, NavbarRight $ MenuItem
|
||||
{ menuItemLabel = "Profile"
|
||||
, menuItemIcon = Just "user"
|
||||
, 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
|
||||
}
|
||||
@ -328,12 +338,24 @@ 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"
|
||||
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
|
||||
@ -363,7 +385,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
|
||||
@ -373,7 +395,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
|
||||
@ -388,13 +410,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
|
||||
@ -419,7 +441,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"
|
||||
|
||||
@ -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,60 +49,61 @@ 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 (CourseEditExistR tid shd ) False
|
||||
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else ""
|
||||
[whamlet|
|
||||
[whamlet|
|
||||
$if adminLink == Authorized
|
||||
<a href=@{CourseEditExistR tid shd}>
|
||||
editieren
|
||||
|]
|
||||
)
|
||||
)
|
||||
]
|
||||
let pageLinks =
|
||||
let pageLinks =
|
||||
[ NavbarLeft $ MenuItem
|
||||
{ menuItemLabel = "Neuer Kurs"
|
||||
, menuItemIcon = Just "book"
|
||||
, menuItemRoute = CourseEditR
|
||||
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False
|
||||
}
|
||||
]
|
||||
]
|
||||
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
|
||||
defaultLinkLayout pageLinks $ do
|
||||
-- defaultLayout $ do
|
||||
setTitle "Semesterkurse"
|
||||
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR
|
||||
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
|
||||
-- defaultLayout $ 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 (UniqueCourseParticipant cid aid)
|
||||
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
|
||||
defaultLayout $ 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
|
||||
@ -110,30 +111,30 @@ postCourseShowR tid csh = do
|
||||
(Entity cid _) <- getBy404 $ CourseTermShort tid csh
|
||||
registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid)
|
||||
return (cid, registered)
|
||||
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
||||
case regResult of
|
||||
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
|
||||
case regResult of
|
||||
(FormSuccess _)
|
||||
| registered -> do
|
||||
runDB $ deleteBy $ UniqueCourseParticipant cid aid
|
||||
addMessage "info" "Sie wurden abgemeldet."
|
||||
| registered -> do
|
||||
runDB $ deleteBy $ UniqueCourseParticipant cid aid
|
||||
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
|
||||
|
||||
getCourseEditR :: Handler Html
|
||||
getCourseEditR = do
|
||||
-- TODO: Defaults für Semester hier ermitteln und übergeben
|
||||
courseEditHandler Nothing
|
||||
|
||||
|
||||
postCourseEditR :: Handler Html
|
||||
postCourseEditR = courseEditHandler Nothing
|
||||
|
||||
|
||||
getCourseEditExistR :: TermId -> Text -> Handler Html
|
||||
getCourseEditExistR tid csh = do
|
||||
getCourseEditExistR tid csh = do
|
||||
course <- runDB $ getBy $ CourseTermShort tid csh
|
||||
courseEditHandler course
|
||||
|
||||
@ -143,28 +144,28 @@ getCourseEditExistIDR cID = do
|
||||
courseID <- UUID.decrypt cIDKey cID
|
||||
courseEditHandler =<< runDB (getEntity courseID)
|
||||
|
||||
|
||||
|
||||
courseEditHandler :: Maybe (Entity Course) -> Handler Html
|
||||
courseEditHandler course = do
|
||||
aid <- requireAuthId
|
||||
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
|
||||
action <- lookupPostParam "formaction"
|
||||
case (result,action) of
|
||||
(FormSuccess res, fAct)
|
||||
(FormSuccess res, fAct)
|
||||
| fAct == formActionDelete
|
||||
, Just cid <- cfCourseId res -> do
|
||||
, Just cid <- cfCourseId res -> do
|
||||
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
|
||||
let cti = toPathPiece $ cfTerm res
|
||||
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
|
||||
redirect $ CourseListTermR $ cfTerm res
|
||||
| fAct == formActionSave
|
||||
, Just cid <- cfCourseId res -> do
|
||||
| fAct == formActionSave
|
||||
, Just cid <- cfCourseId res -> do
|
||||
let tid = cfTerm res
|
||||
actTime <- liftIO getCurrentTime
|
||||
updateokay <- runDB $ do
|
||||
exists <- getBy $ CourseTermShort tid $ cfShort res
|
||||
exists <- getBy $ CourseTermShort tid $ cfShort res
|
||||
let upokay = isNothing exists
|
||||
when upokay $ update cid
|
||||
when upokay $ update cid
|
||||
[ CourseName =. cfName res
|
||||
, CourseDescription =. cfDesc res
|
||||
, CourseLinkExternal =. cfLink res
|
||||
@ -179,17 +180,17 @@ courseEditHandler course = do
|
||||
]
|
||||
return upokay
|
||||
let cti = toPathPiece $ cfTerm res
|
||||
if updateokay
|
||||
then do
|
||||
if updateokay
|
||||
then do
|
||||
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |]
|
||||
redirect $ CourseListTermR $ cfTerm res
|
||||
else do
|
||||
addMessage "danger" [shamlet| Kurs #{cti}/#{cfShort res} konnte nicht geändert werden.
|
||||
addMessage "danger" [shamlet| Kurs #{cti}/#{cfShort res} konnte nicht geändert werden.
|
||||
\ Es gibt bereits einen anderen Kurs mit diesem Kürzel in diesem Semester.|]
|
||||
| fAct == formActionSave
|
||||
, Nothing <- cfCourseId res -> do
|
||||
actTime <- liftIO getCurrentTime
|
||||
insertOkay <- runDB $ insertUnique $ Course
|
||||
insertOkay <- runDB $ insertUnique $ Course
|
||||
{ courseName = cfName res
|
||||
, courseDescription = cfDesc res
|
||||
, courseLinkExternal = cfLink res
|
||||
@ -204,17 +205,17 @@ courseEditHandler course = do
|
||||
, courseChanged = actTime
|
||||
, courseCreatedBy = aid
|
||||
, courseChangedBy = aid
|
||||
}
|
||||
case insertOkay of
|
||||
}
|
||||
case insertOkay of
|
||||
(Just cid) -> do
|
||||
runDB $ insert_ $ Lecturer aid cid
|
||||
runDB $ insert_ $ Lecturer aid cid
|
||||
let cti = toPathPiece $ cfTerm res
|
||||
addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|]
|
||||
redirect $ CourseListTermR $ cfTerm res
|
||||
Nothing -> do
|
||||
let cti = toPathPiece $ cfTerm res
|
||||
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
|
||||
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
|
||||
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
|
||||
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
|
||||
_other -> return ()
|
||||
let formTitle = "Kurs editieren/anlegen" :: Text
|
||||
let actionUrl = CourseEditR
|
||||
@ -222,28 +223,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
|
||||
@ -253,26 +254,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)
|
||||
@ -282,9 +283,9 @@ newCourseForm template = identForm FIDcourse $ \html -> do
|
||||
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
|
||||
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
|
||||
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
|
||||
-- <* bootstrapSubmit (bsSubmit (show cid))
|
||||
return $ case result of
|
||||
FormSuccess courseResult
|
||||
-- <* bootstrapSubmit (bsSubmit (show cid))
|
||||
return $ case result of
|
||||
FormSuccess courseResult
|
||||
| errorMsgs <- validateCourse courseResult
|
||||
, not $ null errorMsgs ->
|
||||
(FormFailure errorMsgs,
|
||||
@ -293,18 +294,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
|
||||
@ -324,5 +325,3 @@ validateCourse (CourseForm{..}) =
|
||||
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
|
||||
)
|
||||
] ]
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,47 +27,47 @@ getTermShowR = do
|
||||
-- term <- runDB $ E.select . E.from $ \(term) -> do
|
||||
-- E.orderBy [E.desc $ term E.^. TermStart ]
|
||||
-- return term
|
||||
--
|
||||
--
|
||||
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 E.countRows
|
||||
return (term, courseCount)
|
||||
selectRep $ do
|
||||
provideRep $ toJSON . map fst <$> runDB (E.select termData)
|
||||
provideRep $ do
|
||||
let colonnadeTerms = mconcat
|
||||
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{..},_) ->
|
||||
|]
|
||||
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
|
||||
stringCell $ formatTimeGerWD termLectureStart
|
||||
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
|
||||
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
|
||||
stringCell $ formatTimeGerWD termLectureEnd
|
||||
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
|
||||
, headed "Aktiv" $ \(Entity _ Term{..},_) ->
|
||||
textCell $ bool "" tickmark termActive
|
||||
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
|
||||
cell [whamlet|
|
||||
<a href=@{CourseListTermR tid}>
|
||||
#{show numCourses} Kurse
|
||||
|]
|
||||
|]
|
||||
, headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
||||
stringCell $ formatTimeGerWD termStart
|
||||
, headed "Semesterende" $ \(Entity _ Term{..},_) ->
|
||||
stringCell $ formatTimeGerWD termEnd
|
||||
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
|
||||
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
|
||||
]
|
||||
]
|
||||
table <- dbTable def $ DBTable
|
||||
{ dbtSQLQuery = termData
|
||||
, dbtColonnade = colonnadeTerms
|
||||
@ -79,30 +79,29 @@ getTermShowR = do
|
||||
setTitle "Freigeschaltete Semester"
|
||||
table
|
||||
|
||||
|
||||
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
|
||||
let tid = termToText $ termName res
|
||||
let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
|
||||
let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
|
||||
addMessage "success" [shamlet| #{msg} |]
|
||||
redirect TermShowR
|
||||
(FormMissing ) -> return ()
|
||||
@ -112,7 +111,7 @@ termEditHandler term = do
|
||||
defaultLayout $ do
|
||||
setTitle [shamlet| #{formTitle} |]
|
||||
$(widgetFile "formPage")
|
||||
|
||||
|
||||
newTermForm :: Maybe Term -> Form Term
|
||||
newTermForm template html = do
|
||||
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
||||
@ -124,8 +123,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,
|
||||
@ -134,13 +133,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
|
||||
-}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -18,6 +18,7 @@ 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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -35,7 +35,7 @@ data FormIdentifier = FIDcourse | FIDsheet
|
||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||
|
||||
|
||||
identForm :: FormIdentifier -> Form a -> Form a
|
||||
identForm :: FormIdentifier -> Form a -> Form a
|
||||
identForm fid = identifyForm (T.pack $ show fid)
|
||||
|
||||
-------------------
|
||||
@ -48,7 +48,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)
|
||||
|
||||
----------------------------
|
||||
@ -67,13 +67,13 @@ class (Enum a, Bounded a, Ord a, PathPiece a) => Button a where
|
||||
|
||||
cssClass :: a -> ButtonCssClass
|
||||
cssClass _ = BCDefault
|
||||
|
||||
|
||||
|
||||
|
||||
{- Abort is not useful (press Back instead); Delete should be different:
|
||||
data StandardButton = BtnDelete | BtnAbort | BtnSave
|
||||
deriving (Enum, Eq, Ord, Bounded, Read, Show)
|
||||
|
||||
|
||||
instance PathPiece StandardButton where -- for displaying the button only, not really for paths
|
||||
toPathPiece = showToPathPiece
|
||||
fromPathPiece = readFromPathPiece
|
||||
@ -82,7 +82,7 @@ instance Button StandardButton where
|
||||
label BtnDelete = "Löschen"
|
||||
label BtnAbort = "Abbrechen"
|
||||
label BtnSave = "Speichern"
|
||||
|
||||
|
||||
cssClass BtnDelete = BCWarning
|
||||
cssClass BtnAbort = BCDefault
|
||||
cssClass BtnSave = BCPrimary
|
||||
@ -99,45 +99,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]
|
||||
@ -146,10 +146,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}
|
||||
@ -171,14 +171,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|
|
||||
@ -199,7 +199,7 @@ buttonForm csrf = do
|
||||
accResult' FormMissing _ = FormMissing
|
||||
accResult' (FormFailure errs) _ = FormFailure errs
|
||||
|
||||
|
||||
|
||||
---------------------------------------
|
||||
-- Buttons (old version, deprecated) --
|
||||
---------------------------------------
|
||||
@ -235,8 +235,8 @@ postButtonForm lblId = identifyForm lblId buttonF
|
||||
buttonF = renderAForm FormStandard $ pure () <* bootstrapSubmit bProps
|
||||
bProps :: BootstrapSubmit Text
|
||||
bProps = fromString $ unpack lblId
|
||||
|
||||
|
||||
|
||||
|
||||
------------
|
||||
-- Fields --
|
||||
------------
|
||||
@ -273,7 +273,7 @@ sheetTypeAFormReq d (Just (Normal p)) =
|
||||
|
||||
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime
|
||||
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
|
||||
utcTimeField = Field
|
||||
utcTimeField = Field
|
||||
{ fieldParse = parseHelper $ readTime
|
||||
, fieldView = \theId name attrs val isReq ->
|
||||
[whamlet|
|
||||
@ -282,42 +282,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"
|
||||
|
||||
@ -334,7 +334,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
|
||||
@ -344,4 +344,3 @@ setTooltip :: String -> FieldSettings site -> FieldSettings site
|
||||
setTooltip tt fs
|
||||
| null tt = fs { fsTooltip = Nothing }
|
||||
| otherwise = fs { fsTooltip = Just $ fromString tt }
|
||||
|
||||
|
||||
@ -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 !!)
|
||||
|
||||
8
src/Handler/Utils/Templates.hs
Normal file
8
src/Handler/Utils/Templates.hs
Normal file
@ -0,0 +1,8 @@
|
||||
{-# LANGUAGE NoImplicitPrelude, TemplateHaskell #-}
|
||||
|
||||
module Handler.Utils.Templates where
|
||||
|
||||
import Import.NoFoundation
|
||||
|
||||
lipsum :: WidgetT site IO ()
|
||||
lipsum = $(widgetFile "widgets/lipsum")
|
||||
@ -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
7022
static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
9
static/css/fonts.css
Normal file
9
static/css/fonts.css
Normal 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');
|
||||
}
|
||||
30
static/css/icons.css
Normal file
30
static/css/icons.css
Normal file
@ -0,0 +1,30 @@
|
||||
.glyphicon {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
.glyphicon::before {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
margin: 0 13px;
|
||||
font-family: 'Glyphicons Halflings';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.glyphicon--home::before {
|
||||
content: '\e021';
|
||||
}
|
||||
.glyphicon--book::before {
|
||||
content: '\e043';
|
||||
}
|
||||
.glyphicon--user::before {
|
||||
content: '\e008';
|
||||
}
|
||||
.glyphicon--login::before {
|
||||
content: '\e161';
|
||||
}
|
||||
.glyphicon--logout::before {
|
||||
content: '\e163';
|
||||
}
|
||||
@ -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
19
templates/course.lucius
Normal 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;
|
||||
}
|
||||
6
templates/courses.hamlet
Normal file
6
templates/courses.hamlet
Normal file
@ -0,0 +1,6 @@
|
||||
<div .container>
|
||||
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
|
||||
^{coursesTable}
|
||||
|
||||
<div .container>
|
||||
<a href=@{CourseEditR}>Neuen Kurs anlegen
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -1,73 +1,200 @@
|
||||
.masthead,
|
||||
.navbar {
|
||||
background-color: rgb(27, 28, 29);
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav > .active > a {
|
||||
background-color: transparent;
|
||||
border-bottom: 2px solid white;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
margin-top: -21px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.masthead .header {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.masthead .btn {
|
||||
margin: 1em 0;
|
||||
}
|
||||
: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;
|
||||
|
||||
|
||||
/* Common styles for all types */
|
||||
.bs-callout {
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border: 1px solid #eee;
|
||||
border-left-width: 5px;
|
||||
border-radius: 3px;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.bs-callout p:last-child {
|
||||
margin-bottom: 0;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bs-callout-info {
|
||||
border-left-color: #1b809e;
|
||||
body {
|
||||
background-color: white;
|
||||
color: var(--fontbase);
|
||||
font-family: var(--fontfamilybase);
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Space things out */
|
||||
.bs-docs-section {
|
||||
margin-bottom: 60px;
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--darkbase);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color .2s ease, background-color .2s ease;
|
||||
}
|
||||
.bs-docs-section:last-child {
|
||||
margin-bottom: 0;
|
||||
a:hover {
|
||||
color: var(--lightbase);
|
||||
}
|
||||
|
||||
#message {
|
||||
margin-bottom: 40px;
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.list--inline > li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
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;
|
||||
/*width: 100%;*/
|
||||
}
|
||||
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;
|
||||
padding-right: 5vw;
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
}
|
||||
|
||||
.main__content {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
padding: 0 40px;
|
||||
padding-right: 0;
|
||||
flex: 1;
|
||||
z-index: 0;
|
||||
|
||||
> .container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pseudo-focus {
|
||||
outline: 5px auto var(--lightbase);
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
/* 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)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
@ -10,4 +10,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}
|
||||
|
||||
@ -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>
|
||||
<a href=@{CourseEditR}>Kurse anlegen
|
||||
editieren und anzeigen
|
||||
<ul>
|
||||
<li .list-group-item>
|
||||
<a href=@{UsersR}>Benutzer Verwaltung
|
||||
|
||||
<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=@{TermShowR}>Semester Verwaltung
|
||||
<a href=@{TermEditR}>Neues Semester anlegen
|
||||
|
||||
<li .list-group-item>
|
||||
Knopf-Test:
|
||||
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
|
||||
^{btnWdgt}
|
||||
|
||||
<li .list-group-item>
|
||||
<a href=@{CourseEditR}>Kurse anlegen
|
||||
editieren und anzeigen
|
||||
|
||||
<li .list-group-item>
|
||||
<a href=@{CourseEditR}>Kurse anlegen, editieren und anzeigen
|
||||
|
||||
<li .list-group-item>
|
||||
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
|
||||
|
||||
<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
0
templates/home.julius
Normal file
1
templates/standalone/inputs.hamlet
Normal file
1
templates/standalone/inputs.hamlet
Normal file
@ -0,0 +1 @@
|
||||
<!-- only here to be able to include inputs using `toWidget` -->
|
||||
177
templates/standalone/inputs.julius
Normal file
177
templates/standalone/inputs.julius
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
306
templates/standalone/inputs.lucius
Normal file
306
templates/standalone/inputs.lucius
Normal 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;
|
||||
}
|
||||
}
|
||||
1
templates/standalone/showHide.hamlet
Normal file
1
templates/standalone/showHide.hamlet
Normal file
@ -0,0 +1 @@
|
||||
<!-- only here to be able to include showHide using `toWidget` -->
|
||||
59
templates/standalone/showHide.julius
Normal file
59
templates/standalone/showHide.julius
Normal 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);
|
||||
});
|
||||
});
|
||||
44
templates/standalone/showHide.lucius
Normal file
44
templates/standalone/showHide.lucius
Normal file
@ -0,0 +1,44 @@
|
||||
.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 {
|
||||
|
||||
}
|
||||
|
||||
.js-show-hide--collapsed > .js-show-hide__target {
|
||||
display: none;
|
||||
}
|
||||
1
templates/standalone/sortable.hamlet
Normal file
1
templates/standalone/sortable.hamlet
Normal file
@ -0,0 +1 @@
|
||||
<!-- only here to be able to include sortable using `toWidget` -->
|
||||
88
templates/standalone/sortable.julius
Normal file
88
templates/standalone/sortable.julius
Normal 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);
|
||||
});
|
||||
});
|
||||
31
templates/standalone/sortable.lucius
Normal file
31
templates/standalone/sortable.lucius
Normal 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);
|
||||
}
|
||||
@ -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
|
||||
|
||||
20
templates/widgets/asidenav.hamlet
Normal file
20
templates/widgets/asidenav.hamlet
Normal file
@ -0,0 +1,20 @@
|
||||
<aside .main__aside>
|
||||
<div .asidenav>
|
||||
<div .asidenav__box--dont-hide>
|
||||
<ul .asidenav__list>
|
||||
$forall menuType <- menuTypes
|
||||
$case menuType
|
||||
$of NavbarLeft (MenuItem label mIcon route _)
|
||||
<li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active>
|
||||
$if isJust mIcon
|
||||
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
|
||||
<a .asidenav__link href=@{route}>#{label}
|
||||
$of _
|
||||
|
||||
<div .asidenav__box>
|
||||
<h3 .asidenav__box-title>WiSe 17/18
|
||||
<ul .asidenav__list.asidenav__list--padded>
|
||||
<li>Vorlesung 1
|
||||
<li>Vorlesung 2
|
||||
<li>Vorlesung 3
|
||||
<li>Vorlesung 4
|
||||
55
templates/widgets/asidenav.julius
Normal file
55
templates/widgets/asidenav.julius
Normal file
@ -0,0 +1,55 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
window.utils.aside = function(el) {
|
||||
var asideEl = el;
|
||||
var collapsed = false;
|
||||
var collClass = 'main__aside--collapsed';
|
||||
var animClass = 'main__aside--transitioning';
|
||||
|
||||
init();
|
||||
function init() {
|
||||
var collLS = window.localStorage.getItem('asidenavCollapsed') === 'true';
|
||||
if (document.body.getBoundingClientRect().width < 999 || collLS) {
|
||||
asideEl.classList.add(collClass);
|
||||
collapsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (collapsed && !hasClass() || !collapsed && hasClass()) {
|
||||
asideEl.classList.add(animClass);
|
||||
asideEl.classList.toggle(collClass, collapsed);
|
||||
window.localStorage.setItem('asidenavCollapsed', collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
function hasClass() {
|
||||
return asideEl.classList.contains(collClass);
|
||||
}
|
||||
|
||||
asideEl.addEventListener('click', function(event) {
|
||||
if (event.target === asideEl) {
|
||||
collapsed = !collapsed;
|
||||
check();
|
||||
}
|
||||
});
|
||||
asideEl.addEventListener('transitionend', function(event) {
|
||||
if (event.propertyName === 'opacity') {
|
||||
asideEl.classList.remove(animClass);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', function() {
|
||||
collapsed = document.body.getBoundingClientRect().width < 999;
|
||||
check();
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
utils.aside(document.querySelector('.main__aside'));
|
||||
|
||||
});
|
||||
94
templates/widgets/asidenav.lucius
Normal file
94
templates/widgets/asidenav.lucius
Normal file
@ -0,0 +1,94 @@
|
||||
.main__aside {
|
||||
flex-shrink: 0;
|
||||
background-color: var(--darkbase);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main__aside--transitioning {
|
||||
transition: width .2s ease;
|
||||
}
|
||||
.main__aside--transitioning .asidenav__box{
|
||||
transition: opacity .2s ease;
|
||||
}
|
||||
|
||||
.main__aside--collapsed:hover {
|
||||
overflow: visible;
|
||||
}
|
||||
.main__aside--collapsed {
|
||||
width: 50px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.asidenav__box {
|
||||
margin: 10px 0;
|
||||
padding: 10px 0;
|
||||
border-bottom: 4px solid var(--whitebase);
|
||||
background-color: var(--darkbase);
|
||||
}
|
||||
|
||||
.asidenav__box-title {
|
||||
padding: 7px 13px;
|
||||
}
|
||||
|
||||
.asidenav__list--padded {
|
||||
padding: 0 13px;
|
||||
}
|
||||
|
||||
.asidenav__list-item {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
color: var(--darkbase);
|
||||
|
||||
.glyphicon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&:not(.asidenav__list-item--active):hover {
|
||||
color: white;
|
||||
background-color: var(--darkbase);
|
||||
|
||||
.asidenav__link {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.asidenav__list-item--active {
|
||||
background-color: var(--darkbase);
|
||||
color: white;
|
||||
|
||||
.asidenav__link {
|
||||
pointer-events: none;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__link {
|
||||
position: relative;
|
||||
display: block;
|
||||
line-height: 50px;
|
||||
margin: 4px 0;
|
||||
padding-left: 54px;
|
||||
color: var(--darkbase);
|
||||
z-index: 1;
|
||||
}
|
||||
7
templates/widgets/breadcrumbs.hamlet
Normal file
7
templates/widgets/breadcrumbs.hamlet
Normal 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}
|
||||
>
|
||||
<li .breadcrumbs__item--active>#{title}
|
||||
17
templates/widgets/breadcrumbs.lucius
Normal file
17
templates/widgets/breadcrumbs.lucius
Normal file
@ -0,0 +1,17 @@
|
||||
.breadcrumbs__container {
|
||||
position: absolute;
|
||||
color: white;
|
||||
left: 340px;
|
||||
top: 7px;
|
||||
z-index: 100;
|
||||
transition: left .2s ease;
|
||||
}
|
||||
.breadcrumbs__container .breadcrumbs__link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 999px) {
|
||||
.breadcrumbs__container {
|
||||
left: 90px;
|
||||
}
|
||||
}
|
||||
9
templates/widgets/form.hamlet
Normal file
9
templates/widgets/form.hamlet
Normal 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}
|
||||
51
templates/widgets/form.julius
Normal file
51
templates/widgets/form.julius
Normal 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;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
0
templates/widgets/form.lucius
Normal file
0
templates/widgets/form.lucius
Normal file
10
templates/widgets/lipsum.hamlet
Normal file
10
templates/widgets/lipsum.hamlet
Normal 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.
|
||||
23
templates/widgets/navbar.hamlet
Normal file
23
templates/widgets/navbar.hamlet
Normal file
@ -0,0 +1,23 @@
|
||||
<div .navbar-container>
|
||||
<nav .navbar.js-sticky-navbar>
|
||||
|
||||
<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>
|
||||
$if isJust mIcon
|
||||
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
|
||||
<a .navbar__link href=@{route}>#{label}
|
||||
$of NavbarSecondary (MenuItem label mIcon route _)
|
||||
<li .navbar__list-item.navbar__list-item--secondary :Just route == mcurrentRoute:.navbar__list-item--active>
|
||||
$if isJust mIcon
|
||||
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
|
||||
<a .navbar__link href=@{route}>#{label}
|
||||
$of _
|
||||
|
||||
<!-- breadcrumbs -->
|
||||
$if not $ Just HomeR == mcurrentRoute
|
||||
^{breadcrumbs}
|
||||
|
||||
<div .navbar__pushdown>
|
||||
28
templates/widgets/navbar.julius
Normal file
28
templates/widgets/navbar.julius
Normal 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();
|
||||
|
||||
});
|
||||
111
templates/widgets/navbar.lucius
Normal file
111
templates/widgets/navbar.lucius
Normal file
@ -0,0 +1,111 @@
|
||||
.navbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
line-height: var(--header-height);
|
||||
padding-right: 5vw;
|
||||
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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navbar .navbar__link {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
padding: 0 13px;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.navbar .navbar__link.glyphicon::before {
|
||||
left: 50%;
|
||||
top: -20px;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar__list-item {
|
||||
position: relative;
|
||||
padding-top: 13px;
|
||||
|
||||
> .glyphicon {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .glyphicon::before {
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.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__list-item--active > .navbar__link {
|
||||
color: var(--darkbase);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: var(--whitebase);
|
||||
}
|
||||
.navbar__list-item--secondary > .navbar__link {
|
||||
color: var(--greybase);
|
||||
}
|
||||
|
||||
.navbar__link {
|
||||
transition: opacity .2s ease;
|
||||
}
|
||||
|
||||
.navbar--sticky {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: var(--header-height-collapsed);
|
||||
line-height: var(--header-height-collapsed);
|
||||
z-index: 100;
|
||||
transition: height 0.2s ease, line-height 0.2s ease;
|
||||
|
||||
.navbar__link {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.breadcrumbs__container {
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
.navbar__pushdown {
|
||||
display: none;
|
||||
background-color: var(--darkbase);
|
||||
height: var(--header-height);
|
||||
}
|
||||
.navbar--sticky + .navbar__pushdown {
|
||||
display: block;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user