Compare commits
131 commits
Author | SHA1 | Date | |
---|---|---|---|
9f647cdf16 | |||
1e07c6f9c3 | |||
3c072d174a | |||
e7653e3184 | |||
eb23583876 | |||
c96f81855b | |||
db32b02811 | |||
46c393552a | |||
bd1fe8fbd8 | |||
a3b3986b76 | |||
99049a88ef | |||
50f2cde09c | |||
1611ec2ef1 | |||
2d1cbbf105 | |||
5a83121431 | |||
a880e66c31 | |||
1364a2e36b | |||
6f18f1b5d8 | |||
7f7f64c8b5 | |||
bf970db801 | |||
cdda4e2eee | |||
a0004946d6 | |||
3b00565bda | |||
62920930b5 | |||
656da87e19 | |||
db16b0d2a7 | |||
068ce7a41c | |||
940b62b6fc | |||
e24e7db641 | |||
805e422eba | |||
012c62dd09 | |||
568b5dc036 | |||
fa110ae820 | |||
2956550a6c | |||
47fe00a8b8 | |||
44288b419c | |||
9d0d6ba249 | |||
c184136f8a | |||
474ed3cf28 | |||
dfafe33e06 | |||
8099912fdc | |||
1a9c782666 | |||
a0b8425911 | |||
c977f2cd53 | |||
660b17f279 | |||
1e7482b30f | |||
fa992bc861 | |||
10b1ed834a | |||
12bed07680 | |||
e69202829a | |||
1df217ac01 | |||
08ed0b65e0 | |||
ddf9b2fada | |||
3830bed42e | |||
0055a4160b | |||
f3b12ded69 | |||
66cb093387 | |||
02fd91cb77 | |||
efdca09b1c | |||
5f4e94bfd2 | |||
e565e8ccd3 | |||
09731053c2 | |||
18ffe8ea4d | |||
41539fe979 | |||
c5e1f524b1 | |||
401c544ef1 | |||
716769d24e | |||
ddf86d901b | |||
b52a055543 | |||
bbe563dee7 | |||
7b2c1681eb | |||
035cc1de3b | |||
c517baac1c | |||
20470aa852 | |||
9f015a2bc0 | |||
67b61227de | |||
3b58b57cc5 | |||
400c9ca8b4 | |||
05c512de46 | |||
0e80d93994 | |||
03cd1487fe | |||
b121d461e2 | |||
41375abdd4 | |||
1b8a07a5f6 | |||
4088735c69 | |||
1f1108a886 | |||
764935adbd | |||
2fd0ff850b | |||
c554715df7 | |||
934457f671 | |||
7cebf92356 | |||
9679234062 | |||
36dea2b5ad | |||
3ff560e07e | |||
4b7a8a7198 | |||
b027125c2e | |||
a0dd673af7 | |||
92373fcfa5 | |||
526e105b8b | |||
1c4212f5d0 | |||
ea47fb8b50 | |||
f934632484 | |||
e141fb4666 | |||
edd82eff55 | |||
d02a010b9b | |||
4a601f804e | |||
2eec12a2e3 | |||
ddcca3104e | |||
da82495fa2 | |||
43a1bb4a79 | |||
9861803d62 | |||
4692e78503 | |||
353f1aa994 | |||
062dd10b99 | |||
8c3a562c7d | |||
1c5939d863 | |||
d32b698545 | |||
69e3d7f267 | |||
bac575b140 | |||
4230c3a6a0 | |||
7e4de762a5 | |||
ed4255595c | |||
fcc02bf36f | |||
e482be4475 | |||
332d5f9953 | |||
ad6d783dea | |||
25a1e00cd7 | |||
484609fb8b | |||
2b4822093a | |||
296fe2dcaf | |||
d6654ffc20 |
54 changed files with 1231 additions and 174 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
dist*
|
||||
static/tmp/
|
||||
static/combined/
|
||||
static/uploads/
|
||||
config/client_session_key.aes
|
||||
*.hi
|
||||
*.o
|
||||
|
@ -19,3 +20,4 @@ cabal.sandbox.config
|
|||
*~
|
||||
\#*
|
||||
lebd.cabal
|
||||
node_modules/
|
||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Danielle McLean
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,17 +1,45 @@
|
|||
User
|
||||
username Text maxlen=190
|
||||
password Text maxlen=79
|
||||
fullName Text maxlen=500
|
||||
email Text maxlen=190
|
||||
note Text
|
||||
UniqueUsername username
|
||||
avatar Text maxlen=190
|
||||
note Markdown sqltype=mediumtext
|
||||
UniqueUser username
|
||||
UniqueEmail email
|
||||
deriving Typeable
|
||||
|
||||
PgpKey
|
||||
userId UserId
|
||||
fingerprint Text maxlen=40
|
||||
UniqueFingerprint fingerprint
|
||||
|
||||
Site
|
||||
name Text sqltype=varchar(255)
|
||||
icon Text sqltype=varchar(255)
|
||||
url Text sqltype=varchar(255)
|
||||
template Text sqltype=varchar(255)
|
||||
|
||||
Profile
|
||||
userId UserId
|
||||
siteId SiteId
|
||||
username Text sqltype=varchar(255)
|
||||
displayName Text Maybe sqltype=varchar(255)
|
||||
|
||||
Entry
|
||||
kind EntryKind maxlen=255
|
||||
name Text Maybe maxlen=255
|
||||
content Markdown sqltype=longtext
|
||||
photo Text Maybe maxlen=190
|
||||
published UTCTime
|
||||
updated UTCTime
|
||||
authorId UserId
|
||||
|
||||
Syndication
|
||||
entryId EntryId
|
||||
profileId ProfileId
|
||||
url Text sqltype=varchar(255)
|
||||
|
||||
EntryCategory
|
||||
entryId EntryId
|
||||
category Category sqltype=varchar(190)
|
||||
UniqueEntryCategory entryId category
|
||||
|
|
|
@ -4,5 +4,15 @@
|
|||
/favicon.ico FaviconR GET
|
||||
/keybase.txt KeybaseR GET
|
||||
/robots.txt RobotsR GET
|
||||
/sitemap.xml SitemapR GET
|
||||
|
||||
/ HomeR GET
|
||||
/avatars/#UserId AvatarR GET
|
||||
/categories/#Category CategoryR GET
|
||||
|
||||
/feed FeedR GET
|
||||
!/#EntryKind/feed FeedKindR GET
|
||||
|
||||
!/#EntryKind EntriesR GET
|
||||
!/#EntryKind/#EntryId EntryR GET
|
||||
!/#EntryKind/#EntryId/#Slug EntryWithSlugR GET
|
||||
|
|
|
@ -32,4 +32,7 @@ database:
|
|||
poolsize: "_env:MYSQL_POOLSIZE:10"
|
||||
|
||||
title: 00dani.me
|
||||
#analytics: UA-YOURCODE
|
||||
app-name: lebd
|
||||
username: dani
|
||||
repository: https://gitlab.com/00dani/lebd
|
||||
fb-app-id: "_env:FB_APP_ID:142105433189339"
|
||||
|
|
6
lebd.svg
Normal file
6
lebd.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>lebd</title>
|
||||
<circle id="bg" fill="#343A40" cx="100" cy="100" r="100"></circle>
|
||||
<text id="l" font-family="Arial Unicode MS" font-size="190" fill="#00A6F9" x="15" y="160">ℒ</text>
|
||||
</svg>
|
After Width: | Height: | Size: 326 B |
28
package-lock.json
generated
Normal file
28
package-lock.json
generated
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "lebd",
|
||||
"version": "1.6.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"docopt": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/docopt/-/docopt-0.6.2.tgz",
|
||||
"integrity": "sha1-so6eIiDaXsSffqW7JKR3h0Be6xE=",
|
||||
"dev": true
|
||||
},
|
||||
"openwebicons": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/openwebicons/-/openwebicons-1.4.3.tgz",
|
||||
"integrity": "sha1-Bs7ri4K3Vqv+mQXhq+X/U/o0Z+k="
|
||||
},
|
||||
"sync-version": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sync-version/-/sync-version-1.0.1.tgz",
|
||||
"integrity": "sha1-Y6aglKmigcUqgA1obqu5ZgH4igs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"docopt": "0.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
package.json
Normal file
24
package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "lebd",
|
||||
"version": "1.6.4",
|
||||
"description": "the codebase backing 00dani.me, an indieweb.org site",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/00dani/lebd"
|
||||
},
|
||||
"author": "Danielle McLean <dani@00dani.me>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/00dani/lebd/issues"
|
||||
},
|
||||
"homepage": "https://gitlab.com/00dani/lebd#README",
|
||||
"devDependencies": {
|
||||
"sync-version": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"version": "sync-version package.yaml && git add ."
|
||||
},
|
||||
"dependencies": {
|
||||
"openwebicons": "^1.4.3"
|
||||
}
|
||||
}
|
18
package.yaml
18
package.yaml
|
@ -1,5 +1,5 @@
|
|||
name: lebd
|
||||
version: "1.0.2"
|
||||
version: "1.6.4"
|
||||
|
||||
dependencies:
|
||||
|
||||
|
@ -46,7 +46,21 @@ dependencies:
|
|||
- time
|
||||
- case-insensitive
|
||||
- wai
|
||||
- libravatar >=0.4 && <0.5
|
||||
|
||||
- blaze-markup >=0.8 && <0.9
|
||||
- conduit-combinators >=1.1 && <1.2
|
||||
- esqueleto >=2.5 && <2.6
|
||||
- friendly-time >=0.4 && <0.5
|
||||
- foreign-store >=0.2 && <0.3
|
||||
- markdown >=0.1 && <0.2
|
||||
- mustache >=2.2 && <2.3
|
||||
- parsec >=3.1 && <3.2
|
||||
- slug >=0.1 && <0.2
|
||||
- split >=0.2 && <0.3
|
||||
- unix >=2.7 && <2.8
|
||||
- yesod-auth-hashdb >=1.6.2 && <1.7
|
||||
- yesod-newsfeed >=1.6 && <1.7
|
||||
- yesod-sitemap >=1.4 && <1.5
|
||||
|
||||
# The library contains all of our application code. The executable
|
||||
# defined below is just a thin wrapper.
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
module Application
|
||||
( getApplicationDev
|
||||
|
@ -47,7 +48,11 @@ import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet,
|
|||
|
||||
-- Import all relevant handler modules here.
|
||||
-- Don't forget to add new modules to your cabal file!
|
||||
import Handler.Avatars
|
||||
import Handler.Common
|
||||
import Handler.Categories
|
||||
import Handler.Entries
|
||||
import Handler.Feed
|
||||
import Handler.Home
|
||||
|
||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||
|
|
|
@ -11,16 +11,27 @@ import Import.NoFoundation
|
|||
import Database.Persist.Sql (ConnectionPool, runSqlPool)
|
||||
import Text.Hamlet (hamletFile)
|
||||
import Text.Jasmine (minifym)
|
||||
import Web.Slug (Slug)
|
||||
|
||||
-- Used only when in "auth-dummy-login" setting is enabled.
|
||||
import Yesod.Auth.Dummy
|
||||
import Yesod.Auth.HashDB (authHashDBWithForm)
|
||||
import qualified Yesod.Auth.Message as AuthMsg
|
||||
|
||||
import Yesod.Auth.OpenId (authOpenId, IdentifierType (Claimed))
|
||||
import Yesod.Default.Util (addStaticContentExternal)
|
||||
import Yesod.Core.Types (Logger)
|
||||
import qualified Yesod.Core.Unsafe as Unsafe
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Text.Encoding as TE
|
||||
|
||||
import Package
|
||||
import Model.Cache ( getCached )
|
||||
import Model.Category ( Category )
|
||||
import Model.Entry ( entryTitle )
|
||||
import Model.Entry.Kind ( EntryKind, allEntryKinds, pluralise )
|
||||
import SchemaOrg.BreadcrumbList ( breadcrumbList )
|
||||
|
||||
import Data.Aeson ( encode )
|
||||
import qualified Text.Blaze.Internal as B
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy.Encoding as E
|
||||
import qualified System.Posix.Files as F
|
||||
|
||||
-- | The foundation datatype for your application. This can be a good place to
|
||||
-- keep settings and values requiring initialization before your application
|
||||
|
@ -37,13 +48,8 @@ data App = App
|
|||
data MenuItem = MenuItem
|
||||
{ menuItemLabel :: Text
|
||||
, menuItemRoute :: Route App
|
||||
, menuItemAccessCallback :: Bool
|
||||
}
|
||||
|
||||
data MenuTypes
|
||||
= NavbarLeft MenuItem
|
||||
| NavbarRight MenuItem
|
||||
|
||||
-- This is where we define all of the routes in our application. For a full
|
||||
-- explanation of the syntax, please see:
|
||||
-- http://www.yesodweb.com/book/routing-and-handlers
|
||||
|
@ -61,20 +67,20 @@ mkYesodData "App" $(parseRoutesFile "config/routes")
|
|||
-- | A convenient synonym for creating forms.
|
||||
type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget)
|
||||
|
||||
sessionLifetime :: Int
|
||||
sessionLifetime = 120 -- minutes
|
||||
|
||||
-- Please see the documentation for the Yesod typeclass. There are a number
|
||||
-- of settings which can be configured by overriding methods here.
|
||||
instance Yesod App where
|
||||
-- Controls the base of generated URLs. For more information on modifying,
|
||||
-- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot
|
||||
approot = ApprootRequest $ \app req ->
|
||||
case appRoot $ appSettings app of
|
||||
Nothing -> getApprootText guessApproot app req
|
||||
Just root -> root
|
||||
approot = ApprootMaster $ fromMaybe "localhost" . appRoot . appSettings
|
||||
|
||||
-- Store session data on the client in encrypted cookies,
|
||||
-- default session idle timeout is 120 minutes
|
||||
makeSessionBackend _ = sslOnlySessions . strictSameSiteSessions $ Just <$> defaultClientSessionBackend
|
||||
120 -- timeout in minutes
|
||||
sessionLifetime
|
||||
"config/client_session_key.aes"
|
||||
|
||||
-- Redirect static requests to a subdomain - this is recommended for best
|
||||
|
@ -94,26 +100,21 @@ instance Yesod App where
|
|||
-- b) Validates that incoming write requests include that token in either a header or POST parameter.
|
||||
-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
|
||||
-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
|
||||
yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
|
||||
yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware . sslOnlyMiddleware sessionLifetime
|
||||
|
||||
defaultLayout widget = do
|
||||
master <- getYesod
|
||||
mmsg <- getMessage
|
||||
|
||||
-- muser <- maybeAuthPair
|
||||
muser <- maybeAuthPair
|
||||
mcurrentRoute <- getCurrentRoute
|
||||
|
||||
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
|
||||
(title, parents) <- breadcrumbs
|
||||
(title, crumbs) <- breadcrumbs
|
||||
let allCrumbs = maybe crumbs (\route -> crumbs ++ [(route, title)]) mcurrentRoute
|
||||
jsonCrumbs <- fmap (E.decodeUtf8 . encode) . withUrlRenderer $ breadcrumbList allCrumbs
|
||||
|
||||
-- Define the menu items of the header.
|
||||
let menuItems = []
|
||||
|
||||
let navbarLeftMenuItems = [x | NavbarLeft x <- menuItems]
|
||||
let navbarRightMenuItems = [x | NavbarRight x <- menuItems]
|
||||
|
||||
let navbarLeftFilteredMenuItems = [x | x <- navbarLeftMenuItems, menuItemAccessCallback x]
|
||||
let navbarRightFilteredMenuItems = [x | x <- navbarRightMenuItems, menuItemAccessCallback x]
|
||||
let navbars = [leftMenuItems, rightMenuItems] <*> [muser]
|
||||
|
||||
-- We break up the default layout into two components:
|
||||
-- default-layout is the contents of the body tag, and
|
||||
|
@ -122,18 +123,16 @@ instance Yesod App where
|
|||
-- you to use normal widget features in default-layout.
|
||||
|
||||
pc <- widgetToPageContent $(widgetFile "default-layout")
|
||||
let globalTitle = toHtml . siteTitle . appSettings $ master
|
||||
hasPageTitle = not . B.null $ pageTitle pc
|
||||
fullTitle = if hasPageTitle then mconcat [pageTitle pc, " ~ ", globalTitle] else globalTitle
|
||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||
|
||||
-- The page to be redirected to when authentication is required.
|
||||
authRoute _ = Just $ AuthR LoginR
|
||||
|
||||
-- Routes not requiring authentication.
|
||||
isAuthorized (AuthR _) _ = return Authorized
|
||||
isAuthorized HomeR _ = return Authorized
|
||||
isAuthorized FaviconR _ = return Authorized
|
||||
isAuthorized KeybaseR _ = return Authorized
|
||||
isAuthorized RobotsR _ = return Authorized
|
||||
isAuthorized (StaticR _) _ = return Authorized
|
||||
isAuthorized _ _ = return Authorized
|
||||
|
||||
-- This function creates static content files in the static folder
|
||||
-- and names them based on a hash of their content. This allows
|
||||
|
@ -142,7 +141,7 @@ instance Yesod App where
|
|||
addStaticContent ext mime content = do
|
||||
master <- getYesod
|
||||
let staticDir = appStaticDir $ appSettings master
|
||||
addStaticContentExternal
|
||||
external <- addStaticContentExternal
|
||||
minifym
|
||||
genFileName
|
||||
staticDir
|
||||
|
@ -150,6 +149,13 @@ instance Yesod App where
|
|||
ext
|
||||
mime
|
||||
content
|
||||
case external of
|
||||
(Just (Right (StaticR (StaticRoute filePath _), _))) -> liftIO $ do
|
||||
let staticPath = ((staticDir ++ "/") ++) . T.unpack . T.intercalate "/" $ filePath
|
||||
mode <- F.fileMode <$> F.getFileStatus staticPath
|
||||
F.setFileMode staticPath $ mode `F.unionFileModes` F.groupReadMode
|
||||
_ -> return ()
|
||||
return external
|
||||
where
|
||||
-- Generate a unique filename based on the content itself
|
||||
genFileName lbs = "autogen-" ++ base64md5 lbs
|
||||
|
@ -163,10 +169,26 @@ instance Yesod App where
|
|||
|
||||
makeLogger = return . appLogger
|
||||
|
||||
leftMenuItems, rightMenuItems :: Maybe (UserId, User) -> [MenuItem]
|
||||
leftMenuItems _ = toMenuItem <$> take 5 allEntryKinds
|
||||
where toMenuItem kind = pluralise kind `MenuItem` EntriesR kind
|
||||
rightMenuItems = loggedOutItems `maybe` loggedInItems
|
||||
where loggedOutItems =
|
||||
[ "log in" `MenuItem` AuthR LoginR
|
||||
]
|
||||
loggedInItems (_id, user) =
|
||||
[ userUsername user `MenuItem` HomeR
|
||||
, "log out" `MenuItem` AuthR LogoutR
|
||||
]
|
||||
|
||||
-- Define breadcrumbs.
|
||||
instance YesodBreadcrumbs App where
|
||||
breadcrumb HomeR = return ("Home", Nothing)
|
||||
breadcrumb (AuthR _) = return ("Login", Just HomeR)
|
||||
breadcrumb (AuthR _) = return ("log in", Just HomeR)
|
||||
breadcrumb (EntriesR kind) = return (pluralise kind, Just HomeR)
|
||||
breadcrumb (EntryR kind entryId) = do
|
||||
(Entity _ entry) <- getCached entryId
|
||||
return (entryTitle entry, Just $ EntriesR kind)
|
||||
breadcrumb (EntryWithSlugR kind entryId _) = breadcrumb $ EntryR kind entryId
|
||||
breadcrumb _ = return ("home", Nothing)
|
||||
|
||||
-- How to run database actions.
|
||||
|
@ -185,25 +207,19 @@ instance YesodAuth App where
|
|||
loginDest _ = HomeR
|
||||
-- Where to send a user after logout
|
||||
logoutDest _ = HomeR
|
||||
-- Override the above two destinations when a Referer: header is present
|
||||
redirectToReferer _ = True
|
||||
|
||||
authenticate creds = runDB $ do
|
||||
x <- getBy . UniqueUsername $ credsIdent creds
|
||||
case x of
|
||||
Just (Entity uid _) -> return $ Authenticated uid
|
||||
Nothing -> Authenticated <$> insert User
|
||||
{ userUsername = credsIdent creds
|
||||
, userFullName = ""
|
||||
, userEmail = ""
|
||||
, userNote = ""
|
||||
}
|
||||
authenticate = fmap toResult . lookupCreds
|
||||
where
|
||||
lookupCreds = runDB . getBy . UniqueUser . credsIdent
|
||||
toResult = UserError AuthMsg.InvalidLogin `maybe` (Authenticated . entityKey)
|
||||
|
||||
-- You can add other plugins like Google Email, email or OAuth here
|
||||
authPlugins app = [authOpenId Claimed []] ++ extraAuthPlugins
|
||||
-- Enable authDummy login if enabled.
|
||||
where extraAuthPlugins = [authDummy | appAuthDummyLogin $ appSettings app]
|
||||
|
||||
authPlugins _ = [authHashDBWithForm loginForm (Just . UniqueUser)]
|
||||
where loginForm :: Route App -> Widget
|
||||
loginForm action = do
|
||||
mtok <- reqToken <$> getRequest
|
||||
setTitle "log in"
|
||||
$(widgetFile "auth/login")
|
||||
authHttpManager = getHttpManager
|
||||
|
||||
-- | Access function to determine if a user is logged in.
|
||||
|
@ -237,3 +253,7 @@ unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
|||
-- https://github.com/yesodweb/yesod/wiki/Sending-email
|
||||
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
|
||||
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
|
||||
userProfile :: User -> Route App
|
||||
userProfile user
|
||||
| userUsername user == siteUsername compileTimeAppSettings = HomeR
|
||||
| otherwise = error "Multiple profile pages are not yet supported"
|
||||
|
|
17
src/Handler/Avatars.hs
Normal file
17
src/Handler/Avatars.hs
Normal file
|
@ -0,0 +1,17 @@
|
|||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Handler.Avatars where
|
||||
|
||||
import Import hiding ( (==.) )
|
||||
|
||||
import Database.Esqueleto
|
||||
import Settings.StaticR ( staticR )
|
||||
|
||||
getAvatarR :: UserId -> Handler ()
|
||||
getAvatarR = responseFrom <=< runDB . select . from . queryAvatar
|
||||
where responseFrom (a:_) = redirect $ staticR ["img", unValue a]
|
||||
responseFrom [] = notFound
|
||||
queryAvatar userId user = do
|
||||
where_ $ user ^. UserId ==. val userId
|
||||
return $ user ^. UserAvatar
|
22
src/Handler/Categories.hs
Normal file
22
src/Handler/Categories.hs
Normal file
|
@ -0,0 +1,22 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Handler.Categories where
|
||||
|
||||
import Import hiding ( on, (==.) )
|
||||
|
||||
import Database.Esqueleto
|
||||
import Widget.Feed ( hFeed )
|
||||
import Model.Category ( Category, asTag )
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
getCategoryR :: Category -> Handler Html
|
||||
getCategoryR tag = do
|
||||
title <- asks $ siteTitle . appSettings
|
||||
entries <- runDB . select . from $ \(entry `InnerJoin` category) -> do
|
||||
on $ entry ^. EntryId ==. category ^. EntryCategoryEntryId
|
||||
where_ $ category ^. EntryCategoryCategory ==. val tag
|
||||
return entry
|
||||
defaultLayout $ do
|
||||
setTitle . toHtml . asTag $ tag
|
||||
T.concat [asTag tag, " ~ ", title] `hFeed` entries
|
|
@ -7,8 +7,15 @@
|
|||
module Handler.Common where
|
||||
|
||||
import Data.FileEmbed (embedFile)
|
||||
import Database.Esqueleto ( (^.) )
|
||||
import qualified Database.Esqueleto as E
|
||||
import Yesod.Sitemap
|
||||
|
||||
import Import
|
||||
|
||||
import Model.Entry.Kind ( allEntryKinds )
|
||||
import Widget.Entry ( entryR )
|
||||
|
||||
-- These handlers embed files in the executable at compile time to avoid a
|
||||
-- runtime dependency, and for efficiency.
|
||||
|
||||
|
@ -21,6 +28,35 @@ getKeybaseR :: Handler TypedContent
|
|||
getKeybaseR = return $ TypedContent typePlain
|
||||
$ toContent $(embedFile "config/keybase.txt")
|
||||
|
||||
getRobotsR :: Handler TypedContent
|
||||
getRobotsR = return $ TypedContent typePlain
|
||||
$ toContent $(embedFile "config/robots.txt")
|
||||
getRobotsR :: Handler Text
|
||||
getRobotsR = robots SitemapR
|
||||
|
||||
getSitemapR :: Handler TypedContent
|
||||
getSitemapR = do
|
||||
categories <- runDB . E.select . E.distinct . E.from $ \ec -> do
|
||||
E.orderBy [E.asc $ ec ^. EntryCategoryCategory]
|
||||
return $ ec ^. EntryCategoryCategory
|
||||
entries <- runDB $ selectList [] [Desc EntryPublished]
|
||||
sitemap $ do
|
||||
yield SitemapUrl
|
||||
{ sitemapLoc = HomeR
|
||||
, sitemapLastMod = Nothing
|
||||
, sitemapChangeFreq = Just Daily
|
||||
, sitemapPriority = Nothing
|
||||
}
|
||||
yieldMany $ sitemapUrl . CategoryR . E.unValue <$> categories
|
||||
yieldMany $ sitemapUrl . EntriesR <$> allEntryKinds
|
||||
yieldMany $ entryToSitemapUrl <$> entries
|
||||
|
||||
sitemapUrl :: a -> SitemapUrl a
|
||||
sitemapUrl loc = SitemapUrl
|
||||
{ sitemapLoc = loc
|
||||
, sitemapLastMod = Nothing
|
||||
, sitemapChangeFreq = Nothing
|
||||
, sitemapPriority = Nothing
|
||||
}
|
||||
|
||||
entryToSitemapUrl :: Entity Entry -> SitemapUrl (Route App)
|
||||
entryToSitemapUrl entry = (sitemapUrl $ entryR entry)
|
||||
{ sitemapLastMod = Just . entryUpdated . entityVal $ entry
|
||||
}
|
||||
|
|
54
src/Handler/Entries.hs
Normal file
54
src/Handler/Entries.hs
Normal file
|
@ -0,0 +1,54 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Handler.Entries where
|
||||
|
||||
import Import
|
||||
|
||||
import Yesod.AtomFeed ( atomLink )
|
||||
|
||||
import Model.Cache ( getCached )
|
||||
import Model.Entry ( entryTitle )
|
||||
import Model.Markdown ( unMarkdown )
|
||||
import Widget.Entry ( entryR, hEntry )
|
||||
import Widget.Feed ( hFeed )
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Model.Entry.Kind as K
|
||||
|
||||
getEntriesR :: K.EntryKind -> Handler Html
|
||||
getEntriesR kind = do
|
||||
entries <- runDB $ selectList [EntryKind ==. kind] [Desc EntryPublished]
|
||||
title <- asks $ siteTitle . appSettings
|
||||
let myTitle = T.concat [K.pluralise kind, " ~ ", title]
|
||||
defaultLayout $ do
|
||||
setTitle . toHtml . K.pluralise $ kind
|
||||
FeedKindR kind `atomLink` myTitle
|
||||
hFeed myTitle entries
|
||||
|
||||
getEntryR :: a -> EntryId -> Handler Html
|
||||
getEntryR _ = renderEntry <=< getCached
|
||||
|
||||
getEntryWithSlugR :: a -> EntryId -> b -> Handler Html
|
||||
getEntryWithSlugR kind = const . getEntryR kind
|
||||
|
||||
renderEntry :: (Entity Entry) -> Handler Html
|
||||
renderEntry entry = do
|
||||
let correctRoute = entryR entry
|
||||
actualRoute <- getCurrentRoute
|
||||
author <- getCached . entryAuthorId $ entityVal entry
|
||||
when (actualRoute /= Just correctRoute) $
|
||||
redirectWith movedPermanently301 correctRoute
|
||||
defaultLayout $ do
|
||||
setTitle . toHtml . entryTitle . entityVal $ entry
|
||||
toWidgetHead [hamlet|
|
||||
<meta name="author" content=#{userFullName $ entityVal author}>
|
||||
<link rel="author" href=@{userProfile $ entityVal author}>
|
||||
<meta name="description" content=#{unMarkdown $ entryContent $ entityVal entry}>
|
||||
<meta property="og:title" content=#{entryTitle $ entityVal entry}>
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:description" content=#{unMarkdown $ entryContent $ entityVal entry}>
|
||||
<meta property="article:author" content=@{userProfile $ entityVal author}>
|
||||
<meta property="article:section" content=#{K.pluralise $ entryKind $ entityVal entry}>
|
||||
|]
|
||||
hEntry entry
|
54
src/Handler/Feed.hs
Normal file
54
src/Handler/Feed.hs
Normal file
|
@ -0,0 +1,54 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Handler.Feed where
|
||||
|
||||
import Import
|
||||
|
||||
import Data.Time.Clock.POSIX ( posixSecondsToUTCTime )
|
||||
import Model.Entry ( entryTitle )
|
||||
import Widget.Entry ( entryR )
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Model.Entry.Kind as K
|
||||
|
||||
getFeedR :: Handler TypedContent
|
||||
getFeedR = do
|
||||
entries <- runDB $ selectList [] [Desc EntryPublished]
|
||||
newsFeed $ toFeed entries
|
||||
|
||||
getFeedKindR :: K.EntryKind -> Handler TypedContent
|
||||
getFeedKindR kind = do
|
||||
entries <- runDB $ selectList [EntryKind ==. kind] [Desc EntryPublished]
|
||||
let basicFeed = toFeed entries
|
||||
newsFeed $ basicFeed
|
||||
{ feedTitle = T.concat [K.pluralise kind, " ~ ", feedTitle basicFeed]
|
||||
, feedDescription = toHtml $ T.concat ["feed of all ", K.pluralise kind]
|
||||
, feedLinkSelf = FeedKindR kind
|
||||
, feedLinkHome = EntriesR kind
|
||||
}
|
||||
|
||||
toFeed :: [Entity Entry] -> Feed (Route App)
|
||||
toFeed entries@(latestEntry:_) = (toFeed [])
|
||||
{ feedEntries = toFeedEntry <$> entries
|
||||
, feedUpdated = entryUpdated $ entityVal latestEntry
|
||||
}
|
||||
toFeed [] = Feed
|
||||
{ feedTitle = siteTitle compileTimeAppSettings
|
||||
, feedLinkSelf = FeedR
|
||||
, feedLinkHome = HomeR
|
||||
, feedAuthor = ""
|
||||
, feedDescription = "sitewide feed of all entries"
|
||||
, feedLanguage = "en-au"
|
||||
, feedUpdated = posixSecondsToUTCTime 0
|
||||
, feedLogo = Nothing
|
||||
, feedEntries = []
|
||||
}
|
||||
|
||||
toFeedEntry :: Entity Entry -> FeedEntry (Route App)
|
||||
toFeedEntry entry = FeedEntry
|
||||
{ feedEntryLink = entryR entry
|
||||
, feedEntryUpdated = entryUpdated $ entityVal entry
|
||||
, feedEntryTitle = entryTitle $ entityVal entry
|
||||
, feedEntryContent = toHtml . entryContent . entityVal $ entry
|
||||
, feedEntryEnclosure = Nothing
|
||||
}
|
|
@ -1,14 +1,23 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
module Handler.Home where
|
||||
|
||||
import Import
|
||||
import Widget.Hcard (hcard)
|
||||
|
||||
import Yesod.AtomFeed ( atomLink )
|
||||
|
||||
import Widget.Card ( hCard )
|
||||
import Widget.Feed ( hFeed )
|
||||
|
||||
getHomeR :: Handler Html
|
||||
getHomeR = do
|
||||
user <- runDB . getBy404 $ UniqueUsername "dani"
|
||||
defaultLayout $(widgetFile "home")
|
||||
settings <- asks appSettings
|
||||
user <- runDB . getBy404 . UniqueUser . siteUsername $ settings
|
||||
let title = siteTitle settings
|
||||
entries <- runDB $ selectList [EntryAuthorId ==. entityKey user] [Desc EntryPublished]
|
||||
defaultLayout $ do
|
||||
atomLink FeedR title
|
||||
$(widgetFile "home")
|
||||
|
|
|
@ -3,4 +3,5 @@ module Import
|
|||
) where
|
||||
|
||||
import Foundation as Import
|
||||
import Settings.StaticR as Import
|
||||
import Import.NoFoundation as Import
|
||||
|
|
16
src/Model.hs
16
src/Model.hs
|
@ -11,6 +11,13 @@ module Model where
|
|||
|
||||
import ClassyPrelude.Yesod
|
||||
import Database.Persist.Quasi
|
||||
import Yesod.Auth.HashDB ( HashDBUser(..) )
|
||||
import Text.Mustache ( (~>) )
|
||||
import qualified Text.Mustache as M
|
||||
|
||||
import Model.Category ( Category )
|
||||
import Model.Entry.Kind ( EntryKind )
|
||||
import Model.Markdown ( Markdown )
|
||||
|
||||
-- You can define all of your database entities in the entities file.
|
||||
-- You can find more information on persistent and how to declare entities
|
||||
|
@ -18,3 +25,12 @@ import Database.Persist.Quasi
|
|||
-- http://www.yesodweb.com/book/persistent/
|
||||
share [mkPersist sqlSettings, mkMigrate "migrateAll"]
|
||||
$(persistFileWith lowerCaseSettings "config/models")
|
||||
|
||||
instance HashDBUser User where
|
||||
userPasswordHash = Just . userPassword
|
||||
setPasswordHash pw u = u { userPassword = pw }
|
||||
|
||||
instance M.ToMustache Profile where
|
||||
toMustache p = M.object
|
||||
[ "username" ~> profileUsername p
|
||||
]
|
||||
|
|
23
src/Model/Cache.hs
Normal file
23
src/Model/Cache.hs
Normal file
|
@ -0,0 +1,23 @@
|
|||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
module Model.Cache ( getCached ) where
|
||||
|
||||
import Data.Typeable ( Typeable )
|
||||
import Database.Persist ( Entity (..), Key (..), PersistStore, PersistRecordBackend, keyToValues )
|
||||
import Yesod ( MonadHandler, HandlerSite, YesodPersist, YesodPersistBackend, cachedBy, get404, liftHandlerT, runDB )
|
||||
|
||||
import qualified Data.ByteString.Char8 as C
|
||||
|
||||
newtype CachedEntity t = CachedEntity { unCachedEntity :: Entity t } deriving Typeable
|
||||
|
||||
getCached :: ( MonadHandler m
|
||||
, YesodPersist (HandlerSite m)
|
||||
, PersistStore (YesodPersistBackend (HandlerSite m))
|
||||
, PersistRecordBackend entity (YesodPersistBackend (HandlerSite m))
|
||||
, Typeable entity
|
||||
) => Key entity -> m (Entity entity)
|
||||
getCached entId = liftHandlerT . cached . runDB . withId . get404 $ entId
|
||||
where key = C.pack . show . keyToValues $ entId
|
||||
withId = fmap $ Entity entId
|
||||
cached = fmap unCachedEntity . cachedBy key . fmap CachedEntity
|
14
src/Model/Category.hs
Normal file
14
src/Model/Category.hs
Normal file
|
@ -0,0 +1,14 @@
|
|||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
module Model.Category where
|
||||
|
||||
import Database.Persist ( PersistField )
|
||||
import Web.Slug ( Slug, unSlug )
|
||||
import Yesod ( PathPiece )
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
newtype Category = Category { unCategory :: Slug }
|
||||
deriving (Eq, Read, Show, PathPiece, PersistField)
|
||||
|
||||
asTag :: Category -> T.Text
|
||||
asTag = T.cons '#' . unSlug . unCategory
|
30
src/Model/Entry.hs
Normal file
30
src/Model/Entry.hs
Normal file
|
@ -0,0 +1,30 @@
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Model.Entry where
|
||||
|
||||
import Model ( Entry, entryName, entryContent )
|
||||
import Model.Markdown ( Markdown(Markdown), unMarkdown )
|
||||
import Data.Maybe ( fromMaybe )
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy as TL
|
||||
|
||||
entryTitle :: Entry -> T.Text
|
||||
entryTitle = fromMaybe <$> TL.toStrict . unMarkdown . shorten 30 . entryContent <*> entryName
|
||||
|
||||
class Shorten a where
|
||||
shorten :: Int -> a -> a
|
||||
|
||||
instance Shorten T.Text where
|
||||
shorten i t
|
||||
| T.compareLength t n == GT = flip T.append "..." . T.take (n - 1) $ t
|
||||
| otherwise = t
|
||||
where n = fromIntegral i
|
||||
|
||||
instance Shorten TL.Text where
|
||||
shorten i t
|
||||
| TL.compareLength t n == GT = flip TL.append "..." . TL.take (n - 1) $ t
|
||||
| otherwise = t
|
||||
where n = fromIntegral i
|
||||
|
||||
instance Shorten Markdown where
|
||||
shorten n (Markdown t) = Markdown $ shorten n t
|
32
src/Model/Entry/Kind.hs
Normal file
32
src/Model/Entry/Kind.hs
Normal file
|
@ -0,0 +1,32 @@
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Model.Entry.Kind where
|
||||
|
||||
import Database.Persist.TH ( derivePersistField )
|
||||
import Yesod.Core.Dispatch ( PathPiece, toPathPiece, fromPathPiece )
|
||||
|
||||
import qualified Data.Text as T
|
||||
import Text.Read ( readMaybe )
|
||||
|
||||
data EntryKind = Note | Article | Photo | Video | Audio
|
||||
| Reply | Repost | Like | Favourite | Bookmark | Quote | Rsvp
|
||||
| Listen | Jam | Watch | Play | Read
|
||||
deriving (Enum, Eq, Read, Show)
|
||||
derivePersistField "EntryKind"
|
||||
|
||||
allEntryKinds :: [EntryKind]
|
||||
allEntryKinds = [Note ..]
|
||||
|
||||
singularise :: T.Text -> Maybe EntryKind
|
||||
singularise "replies" = Just Reply
|
||||
singularise "watches" = Just Watch
|
||||
singularise k = readMaybe . T.unpack . T.toTitle . T.init $ k
|
||||
|
||||
pluralise :: EntryKind -> T.Text
|
||||
pluralise Reply = "replies"
|
||||
pluralise Watch = "watches"
|
||||
pluralise k = T.toLower . flip T.snoc 's' . T.pack . show $ k
|
||||
|
||||
instance PathPiece EntryKind where
|
||||
toPathPiece = pluralise
|
||||
fromPathPiece = singularise
|
38
src/Model/Markdown.hs
Normal file
38
src/Model/Markdown.hs
Normal file
|
@ -0,0 +1,38 @@
|
|||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Model.Markdown ( Markdown(..) ) where
|
||||
|
||||
import Data.Aeson ( FromJSON(..), ToJSON(..), Value(Object), object, (.=), (.:) )
|
||||
import Data.Default ( def )
|
||||
import Database.Persist ( PersistField(..), PersistValue(PersistText) )
|
||||
import Database.Persist.Sql ( PersistFieldSql(..), SqlType(SqlString) )
|
||||
import Data.String ( IsString )
|
||||
import Text.Blaze ( ToMarkup(..) )
|
||||
import Text.Markdown ( markdown )
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy as TL
|
||||
|
||||
newtype Markdown = Markdown { unMarkdown :: TL.Text }
|
||||
deriving (Eq, Ord, Monoid, IsString, Show)
|
||||
|
||||
instance ToMarkup Markdown where
|
||||
toMarkup (Markdown t) = markdown def t
|
||||
|
||||
instance PersistField Markdown where
|
||||
toPersistValue (Markdown t) = PersistText $ TL.toStrict t
|
||||
fromPersistValue (PersistText t) = Right . Markdown $ TL.fromStrict t
|
||||
fromPersistValue wrongValue = Left $ T.concat
|
||||
[ "Model.Markdown: When attempting to create Markdown from a PersistValue, received "
|
||||
, T.pack $ show wrongValue
|
||||
, " when a value of type PersistText was expected."
|
||||
]
|
||||
instance PersistFieldSql Markdown where
|
||||
sqlType _ = SqlString
|
||||
|
||||
instance ToJSON Markdown where
|
||||
toJSON (Markdown text) = object ["markdown" .= text]
|
||||
|
||||
instance FromJSON Markdown where
|
||||
parseJSON (Object v) = Markdown <$> v .: "markdown"
|
||||
parseJSON _ = mempty
|
21
src/Package.hs
Normal file
21
src/Package.hs
Normal file
|
@ -0,0 +1,21 @@
|
|||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Package ( Package(..)
|
||||
, Repository(..)
|
||||
, package
|
||||
) where
|
||||
|
||||
import Data.Aeson ( eitherDecodeStrict )
|
||||
import Data.Either ( either )
|
||||
import Language.Haskell.TH.Syntax ( addDependentFile, lift, runIO )
|
||||
import Package.Types
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
|
||||
package :: Package
|
||||
package = $(do
|
||||
let f = "package.json"
|
||||
addDependentFile f
|
||||
json <- runIO $ B.readFile f
|
||||
let result = eitherDecodeStrict json :: Either String Package
|
||||
either fail lift result)
|
27
src/Package/Types.hs
Normal file
27
src/Package/Types.hs
Normal file
|
@ -0,0 +1,27 @@
|
|||
{-# LANGUAGE DeriveLift #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Package.Types where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types ( fieldLabelModifier )
|
||||
import Data.Aeson.TH ( deriveJSON )
|
||||
|
||||
import Data.Char ( toLower )
|
||||
import Language.Haskell.TH.Syntax ( Lift )
|
||||
import Util ( mapFirst )
|
||||
|
||||
data Package = Package
|
||||
{ packageName :: !String
|
||||
, packageVersion :: !String
|
||||
, packageRepository :: !Repository
|
||||
} deriving (Show, Lift)
|
||||
|
||||
data Repository = Repository
|
||||
{ repositoryType :: !String
|
||||
, repositoryUrl :: !String
|
||||
} deriving (Show, Lift)
|
||||
|
||||
$(deriveJSON defaultOptions { fieldLabelModifier = mapFirst toLower . drop 7 } ''Package)
|
||||
$(deriveJSON defaultOptions { fieldLabelModifier = mapFirst toLower . drop 10 } ''Repository)
|
21
src/SchemaOrg/BreadcrumbList.hs
Normal file
21
src/SchemaOrg/BreadcrumbList.hs
Normal file
|
@ -0,0 +1,21 @@
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module SchemaOrg.BreadcrumbList ( breadcrumbList ) where
|
||||
|
||||
import Data.Aeson
|
||||
import qualified Data.Text as T
|
||||
|
||||
breadcrumbList :: [(a, T.Text)] -> (a -> [(T.Text, T.Text)] -> T.Text) -> Value
|
||||
breadcrumbList crumbs url = object
|
||||
[ ("@context", "http://schema.org")
|
||||
, ("@type", "BreadcrumbList")
|
||||
, "itemListElement" .= zipWith (listItem url) [1 :: Int ..] crumbs
|
||||
]
|
||||
listItem :: (a -> [(T.Text, T.Text)] -> T.Text) -> Int -> (a, T.Text) -> Value
|
||||
listItem url i (r, t) = object
|
||||
[ ("@type", "ListItem")
|
||||
, "position" .= i
|
||||
, "item" .= object
|
||||
[ "@id" .= url r []
|
||||
, "name" .= t
|
||||
]
|
||||
]
|
|
@ -19,9 +19,8 @@ import Data.Yaml (decodeEither')
|
|||
import Database.Persist.MySQL (MySQLConf (..))
|
||||
import Language.Haskell.TH.Syntax (Exp, Name, Q)
|
||||
import Network.Wai.Handler.Warp (HostPreference)
|
||||
import Text.Hamlet (HamletSettings(hamletNewlines), NewlineStyle(AlwaysNewlines), defaultHamletSettings)
|
||||
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
|
||||
import Yesod.Default.Util (WidgetFileSettings(wfsHamletSettings), widgetFileNoReload,
|
||||
import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload,
|
||||
widgetFileReload)
|
||||
import qualified Database.MySQL.Base as MySQL
|
||||
|
||||
|
@ -59,11 +58,14 @@ data AppSettings = AppSettings
|
|||
, appSkipCombining :: Bool
|
||||
-- ^ Perform no stylesheet/script combining
|
||||
|
||||
-- Example app-specific configuration values.
|
||||
, appAnalytics :: Maybe Text
|
||||
-- ^ Google Analytics code
|
||||
, appTitle :: Maybe Text
|
||||
, appFacebookId :: Maybe Int
|
||||
-- ^ Facebook app ID.
|
||||
|
||||
, siteTitle :: Text
|
||||
-- ^ Site-wide title.
|
||||
, siteUsername :: Text
|
||||
-- ^ Username of the site's main user, whose h-card will appear on the
|
||||
-- homepage.
|
||||
|
||||
, appAuthDummyLogin :: Bool
|
||||
-- ^ Indicate if auth dummy login should be enabled.
|
||||
|
@ -92,8 +94,9 @@ instance FromJSON AppSettings where
|
|||
appMutableStatic <- o .:? "mutable-static" .!= defaultDev
|
||||
appSkipCombining <- o .:? "skip-combining" .!= defaultDev
|
||||
|
||||
appAnalytics <- o .:? "analytics"
|
||||
appTitle <- o .:? "title"
|
||||
appFacebookId <- o .:? "fb-app-id"
|
||||
siteTitle <- o .: "title"
|
||||
siteUsername <- o .: "username"
|
||||
|
||||
-- This code enables MySQL's strict mode, without which MySQL will truncate data.
|
||||
-- See https://github.com/yesodweb/persistent/wiki/Database-Configuration#strict-mode for details
|
||||
|
@ -116,7 +119,7 @@ instance FromJSON AppSettings where
|
|||
--
|
||||
-- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile
|
||||
widgetFileSettings :: WidgetFileSettings
|
||||
widgetFileSettings = def { wfsHamletSettings = defaultHamletSettings { hamletNewlines = AlwaysNewlines } }
|
||||
widgetFileSettings = def
|
||||
|
||||
-- | How static files should be combined.
|
||||
combineSettings :: CombineSettings
|
||||
|
|
21
src/Settings/StaticR.hs
Normal file
21
src/Settings/StaticR.hs
Normal file
|
@ -0,0 +1,21 @@
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Settings.StaticR ( staticR ) where
|
||||
|
||||
import Foundation ( App, Route(StaticR) )
|
||||
import Settings ( appStaticDir, compileTimeAppSettings )
|
||||
import Yesod.Static ( Route(StaticRoute) )
|
||||
|
||||
import Settings.StaticR.TH ( mkHashMap )
|
||||
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Text as T
|
||||
|
||||
staticR :: [T.Text] -> Route App
|
||||
staticR pieces = StaticR $ StaticRoute pieces params
|
||||
where params = case pieces `M.lookup` staticMap of
|
||||
Just etag -> [("etag", etag)]
|
||||
Nothing -> []
|
||||
|
||||
staticMap :: M.Map [T.Text] T.Text
|
||||
staticMap = M.fromList $(mkHashMap . appStaticDir $ compileTimeAppSettings)
|
25
src/Settings/StaticR/TH.hs
Normal file
25
src/Settings/StaticR/TH.hs
Normal file
|
@ -0,0 +1,25 @@
|
|||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
module Settings.StaticR.TH ( mkHashMap ) where
|
||||
|
||||
import Conduit hiding ( lift )
|
||||
import Data.List.Split ( splitOn )
|
||||
import Language.Haskell.TH.Syntax ( Q, Exp, lift, runIO )
|
||||
import Yesod.Static ( base64md5 )
|
||||
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
|
||||
base64md5File :: MonadIO m => FilePath -> m String
|
||||
base64md5File = fmap base64md5 . liftIO . L.readFile
|
||||
|
||||
genHashPair :: MonadIO m => FilePath -> m ([String], String)
|
||||
genHashPair fp = (tail $ splitOn "/" fp,) <$> base64md5File fp
|
||||
|
||||
genHashMap :: FilePath -> IO [([String], String)]
|
||||
genHashMap dir = runConduitRes
|
||||
$ sourceDirectoryDeep True dir
|
||||
.| mapMC genHashPair
|
||||
.| sinkList
|
||||
|
||||
mkHashMap :: FilePath -> Q Exp
|
||||
mkHashMap fp = lift =<< runIO (genHashMap fp)
|
26
src/Util.hs
Normal file
26
src/Util.hs
Normal file
|
@ -0,0 +1,26 @@
|
|||
module Util ( compileMustache, entityToTuple, mapFirst ) where
|
||||
|
||||
import Database.Persist ( Entity(..), Key )
|
||||
import Text.Mustache ( Template(..), compileTemplate )
|
||||
import Text.Mustache.Types ( Node(TextBlock) )
|
||||
import Text.Parsec.Error ( ParseError )
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.HashMap.Lazy as M
|
||||
|
||||
mapFirst :: (a -> a) -> [a] -> [a]
|
||||
mapFirst f (x:xs) = f x : xs
|
||||
mapFirst _ [] = []
|
||||
|
||||
compileMustache :: String -> T.Text -> Template
|
||||
compileMustache n = either errorTemplate id . compileTemplate n
|
||||
|
||||
errorTemplate :: ParseError -> Template
|
||||
errorTemplate err = Template
|
||||
{ name = "error"
|
||||
, ast = [TextBlock . T.pack $ show err]
|
||||
, partials = M.empty
|
||||
}
|
||||
|
||||
entityToTuple :: Entity t -> (Key t, t)
|
||||
entityToTuple (Entity key value) = (key, value)
|
56
src/Widget/Card.hs
Normal file
56
src/Widget/Card.hs
Normal file
|
@ -0,0 +1,56 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Widget.Card ( hCard ) where
|
||||
|
||||
import Import
|
||||
|
||||
import Database.Esqueleto ( (^.) )
|
||||
import qualified Database.Esqueleto as E
|
||||
|
||||
import Model.Markdown ( unMarkdown )
|
||||
import Text.Mustache ( substitute )
|
||||
import Util ( compileMustache )
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
prettyPgp :: PgpKey -> Text
|
||||
prettyPgp = T.unwords . T.chunksOf 4 . pgpKeyFingerprint
|
||||
|
||||
routeFromPgp :: PgpKey -> Route App
|
||||
routeFromPgp PgpKey { pgpKeyFingerprint = f } = staticR ["pgp", T.takeEnd 8 f ++ ".asc"]
|
||||
|
||||
profileUrl :: Site -> Profile -> Text
|
||||
profileUrl site = substitute $ T.unpack (siteName site) `compileMustache` siteTemplate site
|
||||
|
||||
hCard :: Entity User -> Widget
|
||||
hCard (Entity userId user) = do
|
||||
let (firstName:lastName) = T.words $ userFullName user
|
||||
mcurrentRoute <- getCurrentRoute
|
||||
userProfiles <- handlerToWidget . runDB . E.select . E.from $ \(profile `E.InnerJoin` site) -> do
|
||||
E.on $ profile ^. ProfileSiteId E.==. site ^. SiteId
|
||||
E.where_ $ profile ^. ProfileUserId E.==. E.val userId
|
||||
E.orderBy [E.asc $ site ^. SiteName]
|
||||
return (site, profile)
|
||||
pgpKeys <- handlerToWidget . runDB $ map entityVal <$> selectList [PgpKeyUserId ==. userId] []
|
||||
let maybeFb = find (\(Entity _ site, _) -> "Facebook" == siteName site) userProfiles
|
||||
|
||||
toWidgetHead [hamlet|
|
||||
<meta name="author" content=#{userFullName user}>
|
||||
<meta name="description" content=#{unMarkdown $ userNote user}>
|
||||
<link rel="author" href=@{HomeR}>
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:title" content="#{userFullName user}">
|
||||
<meta property="og:description" content=#{unMarkdown $ userNote user}>
|
||||
<meta property="og:image" content=@{staticR ["img", userAvatar user]}>
|
||||
<meta property="profile:first_name" content=#{firstName}>
|
||||
<meta property="profile:last_name" content=#{T.unwords lastName}>
|
||||
<meta property="profile:username" content=#{userUsername user}>
|
||||
$maybe (_, Entity _ fb) <- maybeFb
|
||||
<meta property="fb:profile_id" content=#{profileUsername fb}>
|
||||
$forall key <- pgpKeys
|
||||
<link rel="pgpkey" type="application/pgp-keys" href=@{routeFromPgp key}>
|
||||
|]
|
||||
|
||||
$(widgetFile "mf2/h-card")
|
42
src/Widget/Entry.hs
Normal file
42
src/Widget/Entry.hs
Normal file
|
@ -0,0 +1,42 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Widget.Entry ( entryR, hEntry ) where
|
||||
|
||||
import Import
|
||||
|
||||
import Database.Esqueleto ( (^.) )
|
||||
import qualified Database.Esqueleto as E
|
||||
import Data.Time.Format ( defaultTimeLocale, formatTime, iso8601DateFormat )
|
||||
import Data.Time.Format.Human ( humanReadableTime )
|
||||
import Model.Entry ( entryTitle )
|
||||
import Web.Slug ( mkSlug )
|
||||
|
||||
data FormattedTime = FormattedTime
|
||||
{ timeUnfriendly :: String
|
||||
, timeFriendly :: String
|
||||
} deriving Eq
|
||||
|
||||
toFormattedTime :: MonadIO m => UTCTime -> m FormattedTime
|
||||
toFormattedTime time = FormattedTime (unfriendly time) <$> friendly time
|
||||
where unfriendly = formatTime defaultTimeLocale . iso8601DateFormat . Just $ "%H:%M:%S%z"
|
||||
friendly = liftIO . humanReadableTime
|
||||
|
||||
entryR :: Entity Entry -> Route App
|
||||
entryR (Entity entryId Entry {..}) = route (entryName >>= mkSlug) entryKind entryId
|
||||
where route (Just s) = \k i -> EntryWithSlugR k i s
|
||||
route Nothing = EntryR
|
||||
|
||||
hEntry :: Entity Entry -> Widget
|
||||
hEntry (Entity entryId entry) = do
|
||||
published <- toFormattedTime . entryPublished $ entry
|
||||
updated <- toFormattedTime . entryUpdated $ entry
|
||||
posses <- handlerToWidget . runDB . E.select . E.from $ \(syndication `E.InnerJoin` profile `E.InnerJoin` site) -> do
|
||||
E.on $ profile ^. ProfileSiteId E.==. site ^. SiteId
|
||||
E.on $ syndication ^. SyndicationProfileId E.==. profile ^. ProfileId
|
||||
E.where_ $ syndication ^. SyndicationEntryId E.==. E.val entryId
|
||||
E.orderBy [E.asc $ site ^. SiteName]
|
||||
return (syndication ^. SyndicationUrl, site ^. SiteIcon, E.coalesceDefault [profile ^. ProfileDisplayName] (profile ^. ProfileUsername))
|
||||
maybeAuthor <- handlerToWidget . runDB . get . entryAuthorId $ entry
|
||||
$(widgetFile "mf2/h-entry")
|
12
src/Widget/Feed.hs
Normal file
12
src/Widget/Feed.hs
Normal file
|
@ -0,0 +1,12 @@
|
|||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Widget.Feed ( hFeed ) where
|
||||
|
||||
import Import
|
||||
import Widget.Entry ( hEntry )
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
hFeed :: T.Text -> [Entity Entry] -> Widget
|
||||
hFeed name entries = do
|
||||
mroute <- getCurrentRoute
|
||||
$(widgetFile "mf2/h-feed")
|
|
@ -1,27 +0,0 @@
|
|||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Widget.Hcard (hcard) where
|
||||
|
||||
import Import
|
||||
import Data.Maybe (fromJust)
|
||||
import qualified Data.Map as M
|
||||
import qualified Network.Libravatar as L
|
||||
|
||||
entityToTuple :: Entity t -> (Key t, t)
|
||||
entityToTuple (Entity key value) = (key, value)
|
||||
|
||||
arrangeProfiles :: M.Map (Key Site) Site -> [Profile] -> [(Site,Profile)]
|
||||
arrangeProfiles sites profiles = sortBy icon $ zip profileSites profiles
|
||||
where findSite = fromJust . flip M.lookup sites . profileSiteId
|
||||
profileSites = findSite <$> profiles
|
||||
icon = comparing $ siteIcon . fst
|
||||
|
||||
hcard :: Entity User -> Widget
|
||||
hcard (Entity userId user) = do
|
||||
maybeAvatar <- liftIO $ L.avatarUrl (L.Email $ userEmail user) def { L.optSecure = True, L.optSize = L.Size 512 }
|
||||
userProfiles <- handlerToWidget . runDB $ do
|
||||
profiles <- map entityVal <$> selectList [ProfileUserId ==. userId] []
|
||||
sites <- M.fromList . map entityToTuple <$> selectList [SiteId <-. (profileSiteId <$> profiles)] []
|
||||
return $ arrangeProfiles sites profiles
|
||||
$(widgetFile "hcard")
|
|
@ -39,8 +39,7 @@ packages:
|
|||
- .
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver
|
||||
# (e.g., acme-missiles-0.3)
|
||||
extra-deps:
|
||||
- libravatar-0.4.0.1
|
||||
extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
flags: {}
|
||||
|
|
BIN
static/img/dewdrop wink.png
Normal file
BIN
static/img/dewdrop wink.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
1
static/openwebicons
Symbolic link
1
static/openwebicons
Symbolic link
|
@ -0,0 +1 @@
|
|||
../node_modules/openwebicons/
|
171
static/pgp/ACA7BABE.asc
Normal file
171
static/pgp/ACA7BABE.asc
Normal file
|
@ -0,0 +1,171 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
6fGcFiSe50Klf2ctCTwAVg==
|
||||
=8wOv
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
14
templates/auth/login.cassius
Normal file
14
templates/auth/login.cassius
Normal file
|
@ -0,0 +1,14 @@
|
|||
body > main
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
.form-control
|
||||
background-color: #1d1f21
|
||||
color: #c9cacc
|
||||
&:focus
|
||||
background-color: #404449
|
||||
color: #c9cacc
|
||||
.input-group-addon
|
||||
background-color: #000
|
||||
color: #fff
|
||||
width: 2.4rem
|
24
templates/auth/login.hamlet
Normal file
24
templates/auth/login.hamlet
Normal file
|
@ -0,0 +1,24 @@
|
|||
<form .container method="post" action=@{action}>
|
||||
$maybe tok <- mtok
|
||||
<input type="hidden" name=#{defaultCsrfParamName} value=#{tok}>
|
||||
|
||||
<div .card.bg-dark>
|
||||
<div .card-body>
|
||||
<div .form-group>
|
||||
<label .sr-only for="auth-login-username">username
|
||||
<div .input-group>
|
||||
<span .input-group-addon aria-hidden>
|
||||
<i .fa.fa-at>
|
||||
<input #auth-login-username .form-control type="text" name="username" placeholder="username" required>
|
||||
|
||||
<div .form-group>
|
||||
<label .sr-only for="auth-login-password">password
|
||||
<div .input-group>
|
||||
<span .input-group-addon aria-hidden>
|
||||
<i .fa.fa-asterisk>
|
||||
<input #auth-login-password .form-control type="password" name="password" placeholder="password" required>
|
||||
|
||||
<div .card-footer>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i .fa.fa-sign-in>
|
||||
log in
|
|
@ -3,50 +3,25 @@ $doctype 5
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="generator" content="#{packageName package} #{packageVersion package}">
|
||||
|
||||
<title>#{pageTitle pc}
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>#{fullTitle}
|
||||
$maybe route <- mcurrentRoute
|
||||
<link rel="canonical" href=@{route}>
|
||||
<meta property="og:url" content=@{route}>
|
||||
$maybe fb <- appFacebookId $ appSettings master
|
||||
<meta property="fb:app_id" content=#{fb}>
|
||||
<link rel="sitemap" href=@{SitemapR}>
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href=@{StaticR openwebicons_css_openwebicons_min_css}>
|
||||
<script src="https://use.fontawesome.com/4fbab4ae27.js">
|
||||
|
||||
^{pageHead pc}
|
||||
|
||||
<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. */
|
||||
/* See the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package for details. */
|
||||
var csrfHeaderName = "#{TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName}";
|
||||
|
||||
var csrfCookieName = "#{TE.decodeUtf8 defaultCsrfCookieName}";
|
||||
var csrfToken = Cookies.get(csrfCookieName);
|
||||
|
||||
|
||||
if (csrfToken) {
|
||||
\ $.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
|
||||
\ if (!options.crossDomain) {
|
||||
\ jqXHR.setRequestHeader(csrfHeaderName, csrfToken);
|
||||
\ }
|
||||
\ });
|
||||
}
|
||||
|
||||
<body>
|
||||
^{pageBody pc}
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous">
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous">
|
||||
$maybe analytics <- appAnalytics $ appSettings master
|
||||
<script>
|
||||
if(!window.location.href.match(/localhost/)){
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '#{analytics}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
|
|
|
@ -1,6 +1,38 @@
|
|||
a
|
||||
color: #00a6f9
|
||||
&:hover
|
||||
color: #0084d6
|
||||
|
||||
[class^="openwebicons-"], [class*=" openwebicons-"]
|
||||
text-decoration: none
|
||||
line-height: 1
|
||||
|
||||
code, kbd, pre, samp
|
||||
font-family: Monoid, Hack, Inconsolata, Menlo, Monaco, Consolas, "Liberation Mono", monospace
|
||||
code, pre
|
||||
color: #cccccc
|
||||
code
|
||||
background-color: #141414
|
||||
|
||||
body
|
||||
background-color: #1d1f21
|
||||
color: #c9cacc
|
||||
display: flex
|
||||
flex-direction: column
|
||||
min-height: 100vh
|
||||
> main
|
||||
flex: 1
|
||||
margin: 2em
|
||||
> footer
|
||||
text-align: center
|
||||
padding-bottom: 1em
|
||||
display: flex
|
||||
justify-content: space-evenly
|
||||
margin: auto 1em
|
||||
#navbar
|
||||
justify-content: space-between
|
||||
.breadcrumb
|
||||
background-color: #404449
|
||||
border-radius: 0
|
||||
.breadcrumb-item.active
|
||||
color: #fff
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
<header>
|
||||
<nav .navbar .navbar-expand-lg .navbar-dark.bg-dark>
|
||||
$maybe title <- appTitle $ appSettings master
|
||||
<a .navbar-brand href=@{HomeR}>#{title}
|
||||
<nav .navbar .navbar-expand-md .navbar-dark.bg-dark>
|
||||
<a .navbar-brand rel="home" href=@{HomeR}>#{siteTitle $ appSettings master}
|
||||
<button type="button" .navbar-toggler data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" aria-label="Toggle navigation">
|
||||
<span .navbar-toggler-icon>
|
||||
|
||||
<div #navbar .collapse.navbar-collapse>
|
||||
<ul .navbar-nav.mr-auto>
|
||||
$forall MenuItem label route _ <- navbarLeftFilteredMenuItems
|
||||
$forall bar <- navbars
|
||||
<ul .navbar-nav>
|
||||
$forall MenuItem label route <- bar
|
||||
<li .nav-item :Just route == mcurrentRoute:.active>
|
||||
<a .nav-link href=@{route}>#{label}
|
||||
|
||||
<ul .navbar-nav>
|
||||
$forall MenuItem label route _ <- navbarRightFilteredMenuItems
|
||||
<li .nav-item :Just route == mcurrentRoute:.active>
|
||||
<a .nav-link href=@{route}>#{label}
|
||||
$if not $ null parents
|
||||
<ul .breadcrumb.rounded-0>
|
||||
$forall bc <- parents
|
||||
$if not $ null crumbs
|
||||
<ol .breadcrumb>
|
||||
$forall (route, title) <- crumbs
|
||||
<li .breadcrumb-item>
|
||||
<a href=@{fst bc}>#{snd bc}
|
||||
<a href=@{route}>#{title}
|
||||
<li .breadcrumb-item.active>#{title}
|
||||
<script type="application/ld+json">#{preEscapedToMarkup jsonCrumbs}
|
||||
$maybe msg <- mmsg
|
||||
<div .alert.alert-info #message>#{msg}
|
||||
|
||||
<main>
|
||||
^{widget}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div .container>
|
||||
<p .text-muted>
|
||||
All content on this site is licensed under a
|
||||
<a rel="license" href="//creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||
<p>
|
||||
all content licensed under #
|
||||
<a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">cc by-sa 4.0
|
||||
$with Package { packageName = n, packageVersion = v, packageRepository = r } <- package
|
||||
<p>
|
||||
powered by #
|
||||
<a rel="code-repository" href="#{repositoryUrl r}/tree/v#{v}">#{n} #{v}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
.card.h-card
|
||||
margin: 2em auto
|
||||
width: 22rem
|
||||
.card-link
|
||||
white-space: nowrap
|
|
@ -1,13 +0,0 @@
|
|||
<div .card.h-card>
|
||||
$maybe avatar <- maybeAvatar
|
||||
<img .card-img-top.u-photo src=#{avatar} alt="Avatar for #{userFullName user}">
|
||||
<div .card-body>
|
||||
<h4 .card-title.p-name>#{userFullName user}
|
||||
<p .card-text.p-note .text-muted>#{userNote user}
|
||||
<a .card-link.u-email rel="me" href="mailto:#{userEmail user}">
|
||||
<i .fa.fa-envelope>
|
||||
#{userEmail user}
|
||||
$forall (site, profile) <- userProfiles
|
||||
<a .card-link.u-url rel="me" href="#{siteUrl site}#{profileUsername profile}">
|
||||
<i .fa.fa-#{siteIcon site}>
|
||||
#{profileUsername profile}
|
22
templates/home.cassius
Normal file
22
templates/home.cassius
Normal file
|
@ -0,0 +1,22 @@
|
|||
body > main
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
> div.h-feed
|
||||
flex: 1
|
||||
margin-bottom: 0
|
||||
> aside.author
|
||||
max-width: 25rem
|
||||
margin-bottom: 2em
|
||||
> .h-card
|
||||
position: sticky
|
||||
top: 1em
|
||||
|
||||
@media (min-width: 768px)
|
||||
body > main
|
||||
flex-direction: row-reverse
|
||||
align-items: unset
|
||||
> div.h-feed
|
||||
margin-right: 2em
|
||||
> aside.author
|
||||
margin-bottom: 0
|
|
@ -1 +1,2 @@
|
|||
^{hcard user}
|
||||
<aside .author>^{hCard user}
|
||||
^{hFeed title entries}
|
||||
|
|
16
templates/mf2/h-card.cassius
Normal file
16
templates/mf2/h-card.cassius
Normal file
|
@ -0,0 +1,16 @@
|
|||
.card.h-card
|
||||
ul.profiles
|
||||
list-style: none
|
||||
padding-left: 0
|
||||
margin-bottom: 0
|
||||
text-align: center
|
||||
> li
|
||||
display: inline-block
|
||||
margin-right: 5px
|
||||
&:last-child
|
||||
margin-right: 0
|
||||
|
||||
/* Provide a little extra space around the badges added by the
|
||||
* http://www.kevinmarks.com/distributed-verify.html script. */
|
||||
.verified, .unverified
|
||||
margin-right: 4px
|
27
templates/mf2/h-card.hamlet
Normal file
27
templates/mf2/h-card.hamlet
Normal file
|
@ -0,0 +1,27 @@
|
|||
<article .card.h-card .bg-dark itemscope itemtype="http://schema.org/Person">
|
||||
$maybe route <- mcurrentRoute
|
||||
<a .u-uid.u-url itemprop="url" href=@{route} hidden>
|
||||
|
||||
<img .card-img-top.u-photo itemprop="image" src=@{AvatarR userId} alt=#{userFullName user}>
|
||||
|
||||
<div .card-body>
|
||||
<h4 .card-title.p-name itemprop="name">#{userFullName user}
|
||||
$forall key <- pgpKeys
|
||||
<a .card-subtitle.u-key type="application/pgp-keys" href=@{routeFromPgp key}>
|
||||
<i .fa.fa-key>
|
||||
#{prettyPgp key}
|
||||
<div .p-note itemprop="description" .text-muted>#{userNote user}
|
||||
|
||||
<ul .profiles>
|
||||
<li>
|
||||
<a .u-email rel="me" itemprop="email" href="mailto:#{userEmail user}">
|
||||
<i .fa.fa-envelope>
|
||||
#{userEmail user}
|
||||
$forall (Entity _ site, Entity _ profile) <- userProfiles
|
||||
<li>
|
||||
<a .u-url rel="me" itemprop="sameAs" href="#{profileUrl site profile}">
|
||||
<i .#{siteIcon site}>
|
||||
$maybe name <- profileDisplayName profile
|
||||
#{name}
|
||||
$nothing
|
||||
#{profileUsername profile}
|
12
templates/mf2/h-entry.cassius
Normal file
12
templates/mf2/h-entry.cassius
Normal file
|
@ -0,0 +1,12 @@
|
|||
article.h-entry
|
||||
.e-content p:last-child
|
||||
margin-bottom: 0
|
||||
> .card-footer
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: space-evenly
|
||||
> *
|
||||
margin-right: 1em
|
||||
> .p-author img
|
||||
height: 1em
|
||||
vertical-align: -0.1em
|
36
templates/mf2/h-entry.hamlet
Normal file
36
templates/mf2/h-entry.hamlet
Normal file
|
@ -0,0 +1,36 @@
|
|||
<article .h-entry .card.bg-dark itemscope itemtype="http://schema.org/BlogPosting">
|
||||
$maybe photo <- entryPhoto entry
|
||||
<img .card-img-top.u-photo itemprop="image" src=@{staticR ["uploads", photo]} alt=#{entryTitle entry}>
|
||||
<div .card-body>
|
||||
$maybe name <- entryName entry
|
||||
<h4 .p-name .card-title itemprop="headline">#{name}
|
||||
<div .e-content itemprop="articleBody">
|
||||
#{entryContent entry}
|
||||
$nothing
|
||||
<div itemprop="headline" hidden>#{entryTitle entry}
|
||||
<div .e-content.p-name itemprop="articleBody">
|
||||
#{entryContent entry}
|
||||
<div .card-footer>
|
||||
$maybe author <- maybeAuthor
|
||||
<a .p-author.h-card href=@{userProfile author}>
|
||||
<img .u-photo src=@{AvatarR $ entryAuthorId entry} alt=#{userFullName author}>
|
||||
#{userFullName author}
|
||||
$# Use a separate hidden block for the schema.org metadata because you
|
||||
$# can't put itemprop="author" and itemprop="url" on the same element,
|
||||
$# because schema.org is garbage.
|
||||
<div hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
|
||||
<a itemprop="url" href=@{userProfile author}>
|
||||
<span itemprop="name">#{userFullName author}
|
||||
<a .u-url itemprop="mainEntityOfPage" href=@{entryR (Entity entryId entry)}>
|
||||
<i .fa.fa-link>
|
||||
permalink
|
||||
<time .dt-published itemprop="datePublished" datetime=#{timeUnfriendly published} title=#{timeUnfriendly published}>
|
||||
<i .fa.fa-calendar>
|
||||
#{timeFriendly published}
|
||||
<time .dt-updated itemprop="dateModified" datetime=#{timeUnfriendly updated} title=#{timeUnfriendly updated} :published == updated:hidden>
|
||||
<i .fa.fa-pencil>
|
||||
#{timeFriendly updated}
|
||||
$forall (E.Value url, E.Value icon, E.Value name) <- posses
|
||||
<a .u-syndication href=#{url}>
|
||||
<i .#{icon}>
|
||||
#{name}
|
5
templates/mf2/h-feed.cassius
Normal file
5
templates/mf2/h-feed.cassius
Normal file
|
@ -0,0 +1,5 @@
|
|||
div.h-feed
|
||||
> ol.list-unstyled
|
||||
margin-bottom: 0
|
||||
> li:not(:last-child)
|
||||
margin-bottom: 1em
|
7
templates/mf2/h-feed.hamlet
Normal file
7
templates/mf2/h-feed.hamlet
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div .h-feed>
|
||||
<span .p-name hidden>#{name}
|
||||
$maybe route <- mroute
|
||||
<a .u-url href=@{route} hidden>
|
||||
<ol .list-unstyled>
|
||||
$forall entry <- entries
|
||||
<li>^{hEntry entry}
|
Loading…
Reference in a new issue