diff --git a/src/Hosted/Hosted.fsproj b/src/Hosted/Hosted.fsproj index beac2ee7..f9fae6cb 100644 --- a/src/Hosted/Hosted.fsproj +++ b/src/Hosted/Hosted.fsproj @@ -12,6 +12,7 @@ + diff --git a/src/Hosted/Main.fs b/src/Hosted/Main.fs index 76ecb961..67d84415 100644 --- a/src/Hosted/Main.fs +++ b/src/Hosted/Main.fs @@ -26,6 +26,8 @@ type EndPoint = | [] Categories | [] AtomFeed | [] RSSFeed + | [] AtomFeedForUser of string + | [] RSSFeedForUser of string | [] Refresh | [] Contact | [] TermsOfUse @@ -158,6 +160,11 @@ module Urls = else sprintf "/user/%s" user let LANG (lang: string) = sprintf "/%s" lang + let RSS_URL user = + if String.IsNullOrEmpty user then + sprintf "/rss" + else + sprintf "/rss/%s.rss" user module Helpers = open System.IO @@ -356,6 +363,7 @@ module Site = type RedirectTemplate = Templating.Template<"../Hosted/redirect.html", serverLoad=Templating.ServerLoad.WhenChanged> type TrainingsTemplate = Templating.Template<"../Hosted/trainings.html", serverLoad=Templating.ServerLoad.WhenChanged> type BlogListTemplate = Templating.Template<"../Hosted/bloglist.html", serverLoad=Templating.ServerLoad.WhenChanged> + type UserBlogListTemplate = Templating.Template<"../Hosted/userbloglist.html", serverLoad=Templating.ServerLoad.WhenChanged> type BlogPostTemplate = Templating.Template<"../Hosted/blogpost.html", serverLoad=Templating.ServerLoad.WhenChanged> type ContactTemplate = Templating.Template<"../Hosted/contact.html", serverLoad=Templating.ServerLoad.WhenChanged> type LegalTemplate = Templating.Template<"../Hosted/legal.html", serverLoad=Templating.ServerLoad.WhenChanged> @@ -1006,11 +1014,78 @@ module Site = .Cookie(Cookies.Banner false) .Doc() |> Content.Page + let USERBLOG_LISTING_NO_PAGING user f = + let templateFile = Path.Combine (__SOURCE_DIRECTORY__, sprintf @"../Hosted/userblog-%s.html" user) + if File.Exists templateFile then + UserBlogListTemplate(templateFile) + else + UserBlogListTemplate() + |> fun template -> + let name = + if String.IsNullOrEmpty(user) then + config.Value.MasterUserDisplayName + else + config.Value.Users.[user] + template + .Menubar(menubar config.Value) + .AuthorName(name) + .AuthorRSSUrl(Urls.RSS_URL user) + .ArticleList(Map.filter f articles.Value |> ARTICLES) + .Pagination(Doc.Empty) + .Footer(MainTemplate.Footer().Doc()) + .Cookie(Cookies.Banner false) + .Doc() + |> Content.Page let REDIRECT_TO (url: string) = RedirectTemplate() .Url(url) .Doc() |> Content.Page + let ARTICLES_BY_USEROPT (userOpt: string option) = + articles.Value |> Map.toList + // Filter by user, if given + |> List.filter (fun ((user, _), _) -> if userOpt.IsSome then user = userOpt.Value else true) + |> List.sortByDescending (fun (_, article: Article) -> article.Date.Ticks) + let ATOM_FEED userOpt = + let ns = XNamespace.Get "http://www.w3.org/2005/Atom" + let articles = ARTICLES_BY_USEROPT userOpt + X (ns + "feed") [] [ + X (ns + "title") [] [TEXT config.Value.Title] + X (ns + "subtitle") [] [TEXT config.Value.Description] + X (ns + "link") ["href" => config.Value.ServerUrl] [] + X (ns + "updated") [] [Helpers.ATOM_DATE DateTime.UtcNow] + for ((user, slug), article) in articles do + X (ns + "entry") [] [ + X (ns + "title") [] [TEXT article.Title] + X (ns + "link") ["href" => config.Value.ServerUrl + Urls.POST_URL (user, slug)] [] + X (ns + "id") [] [TEXT (user+slug)] + for category in article.Categories do + X (ns + "category") [] [TEXT category] + X (ns + "summary") [] [TEXT article.Abstract] + X (ns + "updated") [] [TEXT <| Helpers.ATOM_DATE article.Date] + ] + ] + let RSS_FEED userOpt = + let articles = ARTICLES_BY_USEROPT userOpt + X (N "rss") ["version" => "2.0"] [ + X (N "channel") [] [ + X (N "title") [] [TEXT config.Value.Title] + X (N "description") [] [TEXT config.Value.Description] + X (N "link") [] [TEXT config.Value.ServerUrl] + X (N "lastBuildDate") [] [Helpers.RSS_DATE DateTime.UtcNow] + for ((user, slug), article) in articles do + X (N "item") [] [ + X (N "title") [] [TEXT article.Title] + X (N "link") [] [TEXT <| config.Value.ServerUrl + Urls.POST_URL (user, slug)] + X (N "guid") ["isPermaLink" => "false"] [TEXT (user+slug)] + for category in article.Categories do + X (N "category") [] [TEXT category] + X (N "description") [] [TEXT article.Abstract] + X (N "pubDate") [] [TEXT <| Helpers.RSS_DATE article.Date] + ] + ] + ] + Application.MultiPage (fun (ctx: Context<_>) -> function | Trainings -> TRAININGS () @@ -1050,10 +1125,7 @@ module Site = ARTICLE ("", p) // All articles by a given user | UserArticle (user, "") -> - BLOG_LISTING_NO_PAGING - <| BlogListTemplate.BlogCategoryBanner() - .Category(user) - .Doc() + USERBLOG_LISTING_NO_PAGING user <| fun (u, _) _ -> user = u | UserArticle (user, p) -> ARTICLE (user, p) @@ -1083,26 +1155,15 @@ module Site = Status = Http.Status.Ok, Headers = [Http.Header.Custom "content-type" "application/atom+xml"], WriteBody = fun stream -> - let ns = XNamespace.Get "http://www.w3.org/2005/Atom" - let articles = - articles.Value |> Map.toList |> List.sortByDescending (fun (_, article: Article) -> article.Date.Ticks) - let doc = - X (ns + "feed") [] [ - X (ns + "title") [] [TEXT config.Value.Title] - X (ns + "subtitle") [] [TEXT config.Value.Description] - X (ns + "link") ["href" => config.Value.ServerUrl] [] - X (ns + "updated") [] [Helpers.ATOM_DATE DateTime.UtcNow] - for ((user, slug), article) in articles do - X (ns + "entry") [] [ - X (ns + "title") [] [TEXT article.Title] - X (ns + "link") ["href" => config.Value.ServerUrl + Urls.POST_URL (user, slug)] [] - X (ns + "id") [] [TEXT (user+slug)] - for category in article.Categories do - X (ns + "category") [] [TEXT category] - X (ns + "summary") [] [TEXT article.Abstract] - X (ns + "updated") [] [TEXT <| Helpers.ATOM_DATE article.Date] - ] - ] + let doc = ATOM_FEED None + doc.Save(stream) + ) + | AtomFeedForUser user -> + Content.Custom ( + Status = Http.Status.Ok, + Headers = [Http.Header.Custom "content-type" "application/atom+xml"], + WriteBody = fun stream -> + let doc = ATOM_FEED (Some user) doc.Save(stream) ) | RSSFeed -> @@ -1110,27 +1171,15 @@ module Site = Status = Http.Status.Ok, Headers = [Http.Header.Custom "content-type" "application/rss+xml"], WriteBody = fun stream -> - let articles = - articles.Value |> Map.toList |> List.sortByDescending (fun (_, article: Article) -> article.Date.Ticks) - let doc = - X (N "rss") ["version" => "2.0"] [ - X (N "channel") [] [ - X (N "title") [] [TEXT config.Value.Title] - X (N "description") [] [TEXT config.Value.Description] - X (N "link") [] [TEXT config.Value.ServerUrl] - X (N "lastBuildDate") [] [Helpers.RSS_DATE DateTime.UtcNow] - for ((user, slug), article) in articles do - X (N "item") [] [ - X (N "title") [] [TEXT article.Title] - X (N "link") [] [TEXT <| config.Value.ServerUrl + Urls.POST_URL (user, slug)] - X (N "guid") ["isPermaLink" => "false"] [TEXT (user+slug)] - for category in article.Categories do - X (N "category") [] [TEXT category] - X (N "description") [] [TEXT article.Abstract] - X (N "pubDate") [] [TEXT <| Helpers.RSS_DATE article.Date] - ] - ] - ] + let doc = RSS_FEED None + doc.Save(stream) + ) + | RSSFeedForUser user -> + Content.Custom ( + Status = Http.Status.Ok, + Headers = [Http.Header.Custom "content-type" "application/rss+xml"], + WriteBody = fun stream -> + let doc = RSS_FEED (Some user) doc.Save(stream) ) | Refresh -> @@ -1221,6 +1270,9 @@ type Website() = // Generate the RSS/Atom feeds RSSFeed AtomFeed + for user in users do + RSSFeedForUser user + AtomFeedForUser user // Generate 404 page Error404 // Generate legal pages diff --git a/src/Hosted/assets/custom.css b/src/Hosted/assets/custom.css index 959bc794..7929b3d1 100644 --- a/src/Hosted/assets/custom.css +++ b/src/Hosted/assets/custom.css @@ -370,3 +370,38 @@ pre:not(.hljs) { border-color: #464646; color: #FFF; } + +.form-ajax .success-box, .form-ajax .error-box { + display: none; + margin-top: 20px; +} +.user-information { + text-align: center; + margin-top: 60px; +} + +.user-name { + font-size: 400%; + margin-bottom: 20px; + line-height: 100%; + color: black; +} + +.user-social i { + color: rgb(102, 102, 102); +} + +.user-top a:not(:first-child) { + margin-left: 10px; +} + +.user-profile-picture { + max-width: 200px; + margin-bottom: 20px; + margin-left: auto; + margin-right: auto; +} + +.user-profile-picture img { + border-radius: 50%; +} \ No newline at end of file diff --git a/src/Hosted/themekit/media/icons/fontawesome/icons.css b/src/Hosted/themekit/media/icons/fontawesome/icons.css index bdeb806d..fbd564e7 100644 --- a/src/Hosted/themekit/media/icons/fontawesome/icons.css +++ b/src/Hosted/themekit/media/icons/fontawesome/icons.css @@ -74,4 +74,8 @@ .fa-spinner:before { content: "\f110"; +} + +.fa-rss:before { + content: "\f09e"; } \ No newline at end of file diff --git a/src/Hosted/userbloglist.html b/src/Hosted/userbloglist.html new file mode 100644 index 00000000..a2a4965a --- /dev/null +++ b/src/Hosted/userbloglist.html @@ -0,0 +1,266 @@ + + + + + + + Blogs + + + + + + + + + + + + + + +
+ + +
+
+ +
+
${AuthorName}
+
+
+
+
+
+ +
+
+
+
+
+

Company and team

+ +
+
+

Help and support

+ +
+
+

Learn more

+ +
+
+

Follow us

+ +

Newsletter

+ +
+
+
+ +
+
+ + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/src/Website/Website.fsproj b/src/Website/Website.fsproj index 890954c3..01ddda78 100644 --- a/src/Website/Website.fsproj +++ b/src/Website/Website.fsproj @@ -22,7 +22,7 @@ - + @@ -33,11 +33,27 @@ + + + + + + + + + + + + + + + +