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*
|
dist*
|
||||||
static/tmp/
|
static/tmp/
|
||||||
static/combined/
|
static/combined/
|
||||||
|
static/uploads/
|
||||||
config/client_session_key.aes
|
config/client_session_key.aes
|
||||||
*.hi
|
*.hi
|
||||||
*.o
|
*.o
|
||||||
|
@ -19,3 +20,4 @@ cabal.sandbox.config
|
||||||
*~
|
*~
|
||||||
\#*
|
\#*
|
||||||
lebd.cabal
|
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
|
User
|
||||||
username Text maxlen=190
|
username Text maxlen=190
|
||||||
|
password Text maxlen=79
|
||||||
fullName Text maxlen=500
|
fullName Text maxlen=500
|
||||||
email Text maxlen=190
|
email Text maxlen=190
|
||||||
note Text
|
avatar Text maxlen=190
|
||||||
UniqueUsername username
|
note Markdown sqltype=mediumtext
|
||||||
|
UniqueUser username
|
||||||
UniqueEmail email
|
UniqueEmail email
|
||||||
deriving Typeable
|
deriving Typeable
|
||||||
|
|
||||||
|
PgpKey
|
||||||
|
userId UserId
|
||||||
|
fingerprint Text maxlen=40
|
||||||
|
UniqueFingerprint fingerprint
|
||||||
|
|
||||||
Site
|
Site
|
||||||
|
name Text sqltype=varchar(255)
|
||||||
icon Text sqltype=varchar(255)
|
icon Text sqltype=varchar(255)
|
||||||
url Text sqltype=varchar(255)
|
template Text sqltype=varchar(255)
|
||||||
|
|
||||||
Profile
|
Profile
|
||||||
userId UserId
|
userId UserId
|
||||||
siteId SiteId
|
siteId SiteId
|
||||||
username Text sqltype=varchar(255)
|
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
|
/favicon.ico FaviconR GET
|
||||||
/keybase.txt KeybaseR GET
|
/keybase.txt KeybaseR GET
|
||||||
/robots.txt RobotsR GET
|
/robots.txt RobotsR GET
|
||||||
|
/sitemap.xml SitemapR GET
|
||||||
|
|
||||||
/ HomeR 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"
|
poolsize: "_env:MYSQL_POOLSIZE:10"
|
||||||
|
|
||||||
title: 00dani.me
|
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
|
name: lebd
|
||||||
version: "1.0.2"
|
version: "1.6.4"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
||||||
|
@ -46,7 +46,21 @@ dependencies:
|
||||||
- time
|
- time
|
||||||
- case-insensitive
|
- case-insensitive
|
||||||
- wai
|
- 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
|
# The library contains all of our application code. The executable
|
||||||
# defined below is just a thin wrapper.
|
# defined below is just a thin wrapper.
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
module Application
|
module Application
|
||||||
( getApplicationDev
|
( getApplicationDev
|
||||||
|
@ -47,7 +48,11 @@ import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet,
|
||||||
|
|
||||||
-- Import all relevant handler modules here.
|
-- Import all relevant handler modules here.
|
||||||
-- Don't forget to add new modules to your cabal file!
|
-- Don't forget to add new modules to your cabal file!
|
||||||
|
import Handler.Avatars
|
||||||
import Handler.Common
|
import Handler.Common
|
||||||
|
import Handler.Categories
|
||||||
|
import Handler.Entries
|
||||||
|
import Handler.Feed
|
||||||
import Handler.Home
|
import Handler.Home
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- 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 Database.Persist.Sql (ConnectionPool, runSqlPool)
|
||||||
import Text.Hamlet (hamletFile)
|
import Text.Hamlet (hamletFile)
|
||||||
import Text.Jasmine (minifym)
|
import Text.Jasmine (minifym)
|
||||||
|
import Web.Slug (Slug)
|
||||||
|
|
||||||
-- Used only when in "auth-dummy-login" setting is enabled.
|
import Yesod.Auth.HashDB (authHashDBWithForm)
|
||||||
import Yesod.Auth.Dummy
|
import qualified Yesod.Auth.Message as AuthMsg
|
||||||
|
|
||||||
import Yesod.Auth.OpenId (authOpenId, IdentifierType (Claimed))
|
|
||||||
import Yesod.Default.Util (addStaticContentExternal)
|
import Yesod.Default.Util (addStaticContentExternal)
|
||||||
import Yesod.Core.Types (Logger)
|
import Yesod.Core.Types (Logger)
|
||||||
import qualified Yesod.Core.Unsafe as Unsafe
|
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
|
-- | The foundation datatype for your application. This can be a good place to
|
||||||
-- keep settings and values requiring initialization before your application
|
-- keep settings and values requiring initialization before your application
|
||||||
|
@ -37,13 +48,8 @@ data App = App
|
||||||
data MenuItem = MenuItem
|
data MenuItem = MenuItem
|
||||||
{ menuItemLabel :: Text
|
{ menuItemLabel :: Text
|
||||||
, menuItemRoute :: Route App
|
, 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
|
-- This is where we define all of the routes in our application. For a full
|
||||||
-- explanation of the syntax, please see:
|
-- explanation of the syntax, please see:
|
||||||
-- http://www.yesodweb.com/book/routing-and-handlers
|
-- http://www.yesodweb.com/book/routing-and-handlers
|
||||||
|
@ -61,20 +67,20 @@ mkYesodData "App" $(parseRoutesFile "config/routes")
|
||||||
-- | A convenient synonym for creating forms.
|
-- | A convenient synonym for creating forms.
|
||||||
type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget)
|
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
|
-- Please see the documentation for the Yesod typeclass. There are a number
|
||||||
-- of settings which can be configured by overriding methods here.
|
-- of settings which can be configured by overriding methods here.
|
||||||
instance Yesod App where
|
instance Yesod App where
|
||||||
-- Controls the base of generated URLs. For more information on modifying,
|
-- Controls the base of generated URLs. For more information on modifying,
|
||||||
-- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot
|
-- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot
|
||||||
approot = ApprootRequest $ \app req ->
|
approot = ApprootMaster $ fromMaybe "localhost" . appRoot . appSettings
|
||||||
case appRoot $ appSettings app of
|
|
||||||
Nothing -> getApprootText guessApproot app req
|
|
||||||
Just root -> root
|
|
||||||
|
|
||||||
-- Store session data on the client in encrypted cookies,
|
-- Store session data on the client in encrypted cookies,
|
||||||
-- default session idle timeout is 120 minutes
|
-- default session idle timeout is 120 minutes
|
||||||
makeSessionBackend _ = sslOnlySessions . strictSameSiteSessions $ Just <$> defaultClientSessionBackend
|
makeSessionBackend _ = sslOnlySessions . strictSameSiteSessions $ Just <$> defaultClientSessionBackend
|
||||||
120 -- timeout in minutes
|
sessionLifetime
|
||||||
"config/client_session_key.aes"
|
"config/client_session_key.aes"
|
||||||
|
|
||||||
-- Redirect static requests to a subdomain - this is recommended for best
|
-- 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.
|
-- 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
|
-- 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.
|
-- 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
|
defaultLayout widget = do
|
||||||
master <- getYesod
|
master <- getYesod
|
||||||
mmsg <- getMessage
|
mmsg <- getMessage
|
||||||
|
|
||||||
-- muser <- maybeAuthPair
|
muser <- maybeAuthPair
|
||||||
mcurrentRoute <- getCurrentRoute
|
mcurrentRoute <- getCurrentRoute
|
||||||
|
|
||||||
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
|
-- 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 navbars = [leftMenuItems, rightMenuItems] <*> [muser]
|
||||||
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]
|
|
||||||
|
|
||||||
-- We break up the default layout into two components:
|
-- We break up the default layout into two components:
|
||||||
-- default-layout is the contents of the body tag, and
|
-- 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.
|
-- you to use normal widget features in default-layout.
|
||||||
|
|
||||||
pc <- widgetToPageContent $(widgetFile "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")
|
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||||
|
|
||||||
-- The page to be redirected to when authentication is required.
|
-- The page to be redirected to when authentication is required.
|
||||||
authRoute _ = Just $ AuthR LoginR
|
authRoute _ = Just $ AuthR LoginR
|
||||||
|
|
||||||
-- Routes not requiring authentication.
|
-- Routes not requiring authentication.
|
||||||
isAuthorized (AuthR _) _ = return Authorized
|
isAuthorized _ _ = return Authorized
|
||||||
isAuthorized HomeR _ = return Authorized
|
|
||||||
isAuthorized FaviconR _ = return Authorized
|
|
||||||
isAuthorized KeybaseR _ = return Authorized
|
|
||||||
isAuthorized RobotsR _ = return Authorized
|
|
||||||
isAuthorized (StaticR _) _ = return Authorized
|
|
||||||
|
|
||||||
-- This function creates static content files in the static folder
|
-- This function creates static content files in the static folder
|
||||||
-- and names them based on a hash of their content. This allows
|
-- 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
|
addStaticContent ext mime content = do
|
||||||
master <- getYesod
|
master <- getYesod
|
||||||
let staticDir = appStaticDir $ appSettings master
|
let staticDir = appStaticDir $ appSettings master
|
||||||
addStaticContentExternal
|
external <- addStaticContentExternal
|
||||||
minifym
|
minifym
|
||||||
genFileName
|
genFileName
|
||||||
staticDir
|
staticDir
|
||||||
|
@ -150,6 +149,13 @@ instance Yesod App where
|
||||||
ext
|
ext
|
||||||
mime
|
mime
|
||||||
content
|
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
|
where
|
||||||
-- Generate a unique filename based on the content itself
|
-- Generate a unique filename based on the content itself
|
||||||
genFileName lbs = "autogen-" ++ base64md5 lbs
|
genFileName lbs = "autogen-" ++ base64md5 lbs
|
||||||
|
@ -163,10 +169,26 @@ instance Yesod App where
|
||||||
|
|
||||||
makeLogger = return . appLogger
|
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.
|
-- Define breadcrumbs.
|
||||||
instance YesodBreadcrumbs App where
|
instance YesodBreadcrumbs App where
|
||||||
breadcrumb HomeR = return ("Home", Nothing)
|
breadcrumb (AuthR _) = return ("log in", Just HomeR)
|
||||||
breadcrumb (AuthR _) = return ("Login", 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)
|
breadcrumb _ = return ("home", Nothing)
|
||||||
|
|
||||||
-- How to run database actions.
|
-- How to run database actions.
|
||||||
|
@ -185,25 +207,19 @@ instance YesodAuth App where
|
||||||
loginDest _ = HomeR
|
loginDest _ = HomeR
|
||||||
-- Where to send a user after logout
|
-- Where to send a user after logout
|
||||||
logoutDest _ = HomeR
|
logoutDest _ = HomeR
|
||||||
-- Override the above two destinations when a Referer: header is present
|
|
||||||
redirectToReferer _ = True
|
|
||||||
|
|
||||||
authenticate creds = runDB $ do
|
authenticate = fmap toResult . lookupCreds
|
||||||
x <- getBy . UniqueUsername $ credsIdent creds
|
where
|
||||||
case x of
|
lookupCreds = runDB . getBy . UniqueUser . credsIdent
|
||||||
Just (Entity uid _) -> return $ Authenticated uid
|
toResult = UserError AuthMsg.InvalidLogin `maybe` (Authenticated . entityKey)
|
||||||
Nothing -> Authenticated <$> insert User
|
|
||||||
{ userUsername = credsIdent creds
|
|
||||||
, userFullName = ""
|
|
||||||
, userEmail = ""
|
|
||||||
, userNote = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
-- You can add other plugins like Google Email, email or OAuth here
|
-- You can add other plugins like Google Email, email or OAuth here
|
||||||
authPlugins app = [authOpenId Claimed []] ++ extraAuthPlugins
|
authPlugins _ = [authHashDBWithForm loginForm (Just . UniqueUser)]
|
||||||
-- Enable authDummy login if enabled.
|
where loginForm :: Route App -> Widget
|
||||||
where extraAuthPlugins = [authDummy | appAuthDummyLogin $ appSettings app]
|
loginForm action = do
|
||||||
|
mtok <- reqToken <$> getRequest
|
||||||
|
setTitle "log in"
|
||||||
|
$(widgetFile "auth/login")
|
||||||
authHttpManager = getHttpManager
|
authHttpManager = getHttpManager
|
||||||
|
|
||||||
-- | Access function to determine if a user is logged in.
|
-- | 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/Sending-email
|
||||||
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
|
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
|
||||||
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
|
-- 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
|
module Handler.Common where
|
||||||
|
|
||||||
import Data.FileEmbed (embedFile)
|
import Data.FileEmbed (embedFile)
|
||||||
|
import Database.Esqueleto ( (^.) )
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import Yesod.Sitemap
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
|
import Model.Entry.Kind ( allEntryKinds )
|
||||||
|
import Widget.Entry ( entryR )
|
||||||
|
|
||||||
-- These handlers embed files in the executable at compile time to avoid a
|
-- These handlers embed files in the executable at compile time to avoid a
|
||||||
-- runtime dependency, and for efficiency.
|
-- runtime dependency, and for efficiency.
|
||||||
|
|
||||||
|
@ -21,6 +28,35 @@ getKeybaseR :: Handler TypedContent
|
||||||
getKeybaseR = return $ TypedContent typePlain
|
getKeybaseR = return $ TypedContent typePlain
|
||||||
$ toContent $(embedFile "config/keybase.txt")
|
$ toContent $(embedFile "config/keybase.txt")
|
||||||
|
|
||||||
getRobotsR :: Handler TypedContent
|
getRobotsR :: Handler Text
|
||||||
getRobotsR = return $ TypedContent typePlain
|
getRobotsR = robots SitemapR
|
||||||
$ toContent $(embedFile "config/robots.txt")
|
|
||||||
|
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 NoImplicitPrelude #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
module Handler.Home where
|
module Handler.Home where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Widget.Hcard (hcard)
|
|
||||||
|
import Yesod.AtomFeed ( atomLink )
|
||||||
|
|
||||||
|
import Widget.Card ( hCard )
|
||||||
|
import Widget.Feed ( hFeed )
|
||||||
|
|
||||||
getHomeR :: Handler Html
|
getHomeR :: Handler Html
|
||||||
getHomeR = do
|
getHomeR = do
|
||||||
user <- runDB . getBy404 $ UniqueUsername "dani"
|
settings <- asks appSettings
|
||||||
defaultLayout $(widgetFile "home")
|
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
|
) where
|
||||||
|
|
||||||
import Foundation as Import
|
import Foundation as Import
|
||||||
|
import Settings.StaticR as Import
|
||||||
import Import.NoFoundation 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 ClassyPrelude.Yesod
|
||||||
import Database.Persist.Quasi
|
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 define all of your database entities in the entities file.
|
||||||
-- You can find more information on persistent and how to declare entities
|
-- 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/
|
-- http://www.yesodweb.com/book/persistent/
|
||||||
share [mkPersist sqlSettings, mkMigrate "migrateAll"]
|
share [mkPersist sqlSettings, mkMigrate "migrateAll"]
|
||||||
$(persistFileWith lowerCaseSettings "config/models")
|
$(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 Database.Persist.MySQL (MySQLConf (..))
|
||||||
import Language.Haskell.TH.Syntax (Exp, Name, Q)
|
import Language.Haskell.TH.Syntax (Exp, Name, Q)
|
||||||
import Network.Wai.Handler.Warp (HostPreference)
|
import Network.Wai.Handler.Warp (HostPreference)
|
||||||
import Text.Hamlet (HamletSettings(hamletNewlines), NewlineStyle(AlwaysNewlines), defaultHamletSettings)
|
|
||||||
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
|
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
|
||||||
import Yesod.Default.Util (WidgetFileSettings(wfsHamletSettings), widgetFileNoReload,
|
import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload,
|
||||||
widgetFileReload)
|
widgetFileReload)
|
||||||
import qualified Database.MySQL.Base as MySQL
|
import qualified Database.MySQL.Base as MySQL
|
||||||
|
|
||||||
|
@ -59,11 +58,14 @@ data AppSettings = AppSettings
|
||||||
, appSkipCombining :: Bool
|
, appSkipCombining :: Bool
|
||||||
-- ^ Perform no stylesheet/script combining
|
-- ^ Perform no stylesheet/script combining
|
||||||
|
|
||||||
-- Example app-specific configuration values.
|
, appFacebookId :: Maybe Int
|
||||||
, appAnalytics :: Maybe Text
|
-- ^ Facebook app ID.
|
||||||
-- ^ Google Analytics code
|
|
||||||
, appTitle :: Maybe Text
|
, siteTitle :: Text
|
||||||
-- ^ Site-wide title.
|
-- ^ Site-wide title.
|
||||||
|
, siteUsername :: Text
|
||||||
|
-- ^ Username of the site's main user, whose h-card will appear on the
|
||||||
|
-- homepage.
|
||||||
|
|
||||||
, appAuthDummyLogin :: Bool
|
, appAuthDummyLogin :: Bool
|
||||||
-- ^ Indicate if auth dummy login should be enabled.
|
-- ^ Indicate if auth dummy login should be enabled.
|
||||||
|
@ -92,8 +94,9 @@ instance FromJSON AppSettings where
|
||||||
appMutableStatic <- o .:? "mutable-static" .!= defaultDev
|
appMutableStatic <- o .:? "mutable-static" .!= defaultDev
|
||||||
appSkipCombining <- o .:? "skip-combining" .!= defaultDev
|
appSkipCombining <- o .:? "skip-combining" .!= defaultDev
|
||||||
|
|
||||||
appAnalytics <- o .:? "analytics"
|
appFacebookId <- o .:? "fb-app-id"
|
||||||
appTitle <- o .:? "title"
|
siteTitle <- o .: "title"
|
||||||
|
siteUsername <- o .: "username"
|
||||||
|
|
||||||
-- This code enables MySQL's strict mode, without which MySQL will truncate data.
|
-- 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
|
-- 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
|
-- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile
|
||||||
widgetFileSettings :: WidgetFileSettings
|
widgetFileSettings :: WidgetFileSettings
|
||||||
widgetFileSettings = def { wfsHamletSettings = defaultHamletSettings { hamletNewlines = AlwaysNewlines } }
|
widgetFileSettings = def
|
||||||
|
|
||||||
-- | How static files should be combined.
|
-- | How static files should be combined.
|
||||||
combineSettings :: CombineSettings
|
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
|
# Dependency packages to be pulled from upstream that are not in the resolver
|
||||||
# (e.g., acme-missiles-0.3)
|
# (e.g., acme-missiles-0.3)
|
||||||
extra-deps:
|
extra-deps: []
|
||||||
- libravatar-0.4.0.1
|
|
||||||
|
|
||||||
# Override default flag values for local packages and extra-deps
|
# Override default flag values for local packages and extra-deps
|
||||||
flags: {}
|
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>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<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}
|
<title>#{fullTitle}
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="author" content="">
|
|
||||||
$maybe route <- mcurrentRoute
|
$maybe route <- mcurrentRoute
|
||||||
<link rel="canonical" href=@{route}>
|
<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="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">
|
<script src="https://use.fontawesome.com/4fbab4ae27.js">
|
||||||
|
|
||||||
^{pageHead pc}
|
^{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>
|
<body>
|
||||||
^{pageBody pc}
|
^{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://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://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">
|
<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
|
body
|
||||||
|
background-color: #1d1f21
|
||||||
|
color: #c9cacc
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
min-height: 100vh
|
min-height: 100vh
|
||||||
> main
|
> main
|
||||||
flex: 1
|
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>
|
<header>
|
||||||
<nav .navbar .navbar-expand-lg .navbar-dark.bg-dark>
|
<nav .navbar .navbar-expand-md .navbar-dark.bg-dark>
|
||||||
$maybe title <- appTitle $ appSettings master
|
<a .navbar-brand rel="home" href=@{HomeR}>#{siteTitle $ appSettings master}
|
||||||
<a .navbar-brand href=@{HomeR}>#{title}
|
|
||||||
<button type="button" .navbar-toggler data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" aria-label="Toggle navigation">
|
<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>
|
<span .navbar-toggler-icon>
|
||||||
|
|
||||||
<div #navbar .collapse.navbar-collapse>
|
<div #navbar .collapse.navbar-collapse>
|
||||||
<ul .navbar-nav.mr-auto>
|
$forall bar <- navbars
|
||||||
$forall MenuItem label route _ <- navbarLeftFilteredMenuItems
|
<ul .navbar-nav>
|
||||||
|
$forall MenuItem label route <- bar
|
||||||
<li .nav-item :Just route == mcurrentRoute:.active>
|
<li .nav-item :Just route == mcurrentRoute:.active>
|
||||||
<a .nav-link href=@{route}>#{label}
|
<a .nav-link href=@{route}>#{label}
|
||||||
|
|
||||||
<ul .navbar-nav>
|
$if not $ null crumbs
|
||||||
$forall MenuItem label route _ <- navbarRightFilteredMenuItems
|
<ol .breadcrumb>
|
||||||
<li .nav-item :Just route == mcurrentRoute:.active>
|
$forall (route, title) <- crumbs
|
||||||
<a .nav-link href=@{route}>#{label}
|
|
||||||
$if not $ null parents
|
|
||||||
<ul .breadcrumb.rounded-0>
|
|
||||||
$forall bc <- parents
|
|
||||||
<li .breadcrumb-item>
|
<li .breadcrumb-item>
|
||||||
<a href=@{fst bc}>#{snd bc}
|
<a href=@{route}>#{title}
|
||||||
<li .breadcrumb-item.active>#{title}
|
<li .breadcrumb-item.active>#{title}
|
||||||
|
<script type="application/ld+json">#{preEscapedToMarkup jsonCrumbs}
|
||||||
$maybe msg <- mmsg
|
$maybe msg <- mmsg
|
||||||
<div .alert.alert-info #message>#{msg}
|
<div .alert.alert-info #message>#{msg}
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
^{widget}
|
^{widget}
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer>
|
<footer>
|
||||||
<div .container>
|
<p>
|
||||||
<p .text-muted>
|
all content licensed under #
|
||||||
All content on this site is licensed under a
|
<a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">cc by-sa 4.0
|
||||||
<a rel="license" href="//creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
$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