Implement archives and sidebar

This commit is contained in:
Mikko Ahlroth 2018-11-03 15:21:40 +02:00
parent ca62b6fe41
commit f21e459760
20 changed files with 296 additions and 29 deletions

View file

@ -1,5 +1,6 @@
defmodule Mebe2.Engine.DB do defmodule Mebe2.Engine.DB do
require Logger require Logger
import Ex2ms
alias Mebe2.Engine.{Utils, SlugUtils, Models} alias Mebe2.Engine.{Utils, SlugUtils, Models}
alias Calendar.DateTime alias Calendar.DateTime
@ -188,6 +189,20 @@ defmodule Mebe2.Engine.DB do
get_post_list(@post_table, [{{{year, month, :_, :_}, :"$1"}, [], [:"$_"]}], first, limit) get_post_list(@post_table, [{{{year, month, :_, :_}, :"$1"}, [], [:"$_"]}], first, limit)
end end
@doc """
Get all months stored in the DB as a list of {year, month} tuples.
"""
@spec get_all_months() :: [{integer, integer}]
def get_all_months() do
ms =
fun do
{{year, month, _, _}, _} -> {year, month}
end
:ets.select_reverse(@post_table, ms)
|> Enum.uniq()
end
@spec get_page(String.t()) :: Models.Page.t() | nil @spec get_page(String.t()) :: Models.Page.t() | nil
def get_page(slug) do def get_page(slug) do
case :ets.match_object(@page_table, {slug, :"$1"}) do case :ets.match_object(@page_table, {slug, :"$1"}) do
@ -209,7 +224,7 @@ defmodule Mebe2.Engine.DB do
get_count(:all, :all) get_count(:all, :all)
end end
@spec get_count(atom, :all | integer | String.t()) :: integer() @spec get_count(atom, :all | integer | String.t() | {integer, integer}) :: integer()
def get_count(type, key) do def get_count(type, key) do
get_meta(type, key, 0) get_meta(type, key, 0)
end end
@ -224,7 +239,8 @@ defmodule Mebe2.Engine.DB do
:ets.insert(@meta_table, {{type, key}, value}) :ets.insert(@meta_table, {{type, key}, value})
end end
@spec get_meta(atom, :all | integer | String.t(), integer | String.t()) :: integer | String.t() @spec get_meta(atom, :all | integer | String.t() | {integer, integer}, integer | String.t()) ::
integer | String.t()
defp get_meta(type, key, default) do defp get_meta(type, key, default) do
case :ets.match_object(@meta_table, {{type, key}, :"$1"}) do case :ets.match_object(@meta_table, {{type, key}, :"$1"}) do
[{{_, _}, value}] -> value [{{_, _}, value}] -> value

View file

@ -10,6 +10,7 @@ defmodule Mebe2.Engine.SlugUtils do
Nil is returned as is. Nil is returned as is.
""" """
@spec slugify(String.t() | nil) :: String.t() | nil
def slugify(nil), do: nil def slugify(nil), do: nil
def slugify(value) do def slugify(value) do
@ -19,6 +20,7 @@ defmodule Mebe2.Engine.SlugUtils do
@doc """ @doc """
Get the author name related to this slug from the DB. Get the author name related to this slug from the DB.
""" """
@spec unslugify_author(String.t()) :: String.t()
def unslugify_author(slug) do def unslugify_author(slug) do
DB.get_author_name(slug) DB.get_author_name(slug)
end end

View file

@ -0,0 +1,24 @@
nav.archives {
background-color: $accent-background;
color: $accent-text;
#archives-heading {
margin-top: 0;
}
a {
color: $accent-link;
}
ul {
list-style-position: inside;
&.archives-year-list {
padding-left: 0;
}
&.archives-month-list {
padding-left: 20px;
}
}
}

View file

@ -8,14 +8,15 @@ html {
body { body {
display: grid; display: grid;
grid-template: 'hd' 'mn' 1fr 'ft' / auto; grid-template: 'hd' 'mn' 1fr 'nav' 'ft' / auto;
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
@media #{$md} { @media #{$md} {
grid-template: 'hd mn' grid-template: 'hd mn' 200px
'hd ft' 'nav mn'
'nav ft'
/ 1fr 3fr; / 1fr 3fr;
} }
@ -68,6 +69,12 @@ body {
} }
} }
// Archives in the sidebar
>nav.archives {
grid-area: nav;
padding: $layout-padding;
}
// Footer after all blog posts // Footer after all blog posts
>footer { >footer {
grid-area: ft; grid-area: ft;

View file

@ -13,6 +13,7 @@
@import './typography'; @import './typography';
@import './media'; @import './media';
@import './base-layout'; @import './base-layout';
@import './archives';
@import './post-list'; @import './post-list';
@import './pagination'; @import './pagination';
@import './post-layout'; @import './post-layout';

View file

@ -0,0 +1,42 @@
defmodule Mebe2.Web.Middleware.Archives do
require Logger
@archives_key :mebe2_archives
defmacro __using__(_opts) do
quote do
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(_env) do
quote do
defoverridable Raxx.Server
@impl Raxx.Server
def handle_head(head, config) do
unquote(__MODULE__).put_archives()
super(head, config)
end
end
end
@doc """
Put yearly and monthly archives into the process storage of the current process.
"""
@spec put_archives() :: :ok
def put_archives() do
months = Mebe2.Engine.DB.get_all_months()
Process.put(@archives_key, months)
:ok
end
@doc """
Get list of tuples {year, month} that have at least one post.
"""
@spec get_archives() :: [{integer, integer}]
def get_archives() do
Process.get(@archives_key, [])
end
end

View file

@ -21,30 +21,6 @@ defmodule Mebe2.Web.Middleware.RequestTime do
super(head, config) super(head, config)
|> unquote(__MODULE__).process_response() |> unquote(__MODULE__).process_response()
end end
@impl Raxx.Server
def handle_data(data, config) do
unquote(__MODULE__).put_response_timer()
super(data, config)
|> unquote(__MODULE__).process_response()
end
@impl Raxx.Server
def handle_tail(tail, config) do
unquote(__MODULE__).put_response_timer()
super(tail, config)
|> unquote(__MODULE__).process_response()
end
@impl Raxx.Server
def handle_info(message, config) do
unquote(__MODULE__).put_response_timer()
super(message, config)
|> unquote(__MODULE__).process_response()
end
end end
end end

View file

@ -2,10 +2,15 @@ defmodule Mebe2.Web.Router do
use Ace.HTTP.Service, port: 2142, cleartext: true use Ace.HTTP.Service, port: 2142, cleartext: true
use Raxx.Logger use Raxx.Logger
use Mebe2.Web.Middleware.RequestTime use Mebe2.Web.Middleware.RequestTime
use Mebe2.Web.Middleware.Archives
use Raxx.Router, [ use Raxx.Router, [
{%{method: :GET, path: ["tag", _tag, "p", _page]}, Mebe2.Web.Routes.Tag}, {%{method: :GET, path: ["tag", _tag, "p", _page]}, Mebe2.Web.Routes.Tag},
{%{method: :GET, path: ["tag", _tag]}, Mebe2.Web.Routes.Tag}, {%{method: :GET, path: ["tag", _tag]}, Mebe2.Web.Routes.Tag},
{%{method: :GET, path: ["archive", _year, _month, "p", _page]}, Mebe2.Web.Routes.Month},
{%{method: :GET, path: ["archive", _year, _month]}, Mebe2.Web.Routes.Month},
{%{method: :GET, path: ["archive", _year, "p", _page]}, Mebe2.Web.Routes.Year},
{%{method: :GET, path: ["archive", _year]}, Mebe2.Web.Routes.Year},
{%{method: :GET, path: ["p", _page]}, Mebe2.Web.Routes.Index}, {%{method: :GET, path: ["p", _page]}, Mebe2.Web.Routes.Index},
{%{method: :GET, path: [_year, _month, _day, _slug]}, Mebe2.Web.Routes.Post}, {%{method: :GET, path: [_year, _month, _day, _slug]}, Mebe2.Web.Routes.Post},
{%{method: :GET, path: [_slug]}, Mebe2.Web.Routes.Page}, {%{method: :GET, path: [_slug]}, Mebe2.Web.Routes.Page},

42
lib/web/routes/month.ex Normal file
View file

@ -0,0 +1,42 @@
defmodule Mebe2.Web.Routes.Month do
use Raxx.Server
@impl Raxx.Server
def handle_request(
%Raxx.Request{path: ["archive", year_str, month_str, "p", page]} = _req,
_state
) do
with {year, _} <- Integer.parse(year_str),
{month, _} <- Integer.parse(month_str) do
Mebe2.Web.Routes.Utils.render_posts(
page,
&post_getter(year, month, &1, &2),
&renderer(year, month, &1, &2, &3, &4)
)
else
_ -> Mebe2.Web.Routes.Utils.render_404()
end
end
@impl Raxx.Server
def handle_request(%Raxx.Request{path: ["archive", year_str, month_str]} = _req, _state) do
with {year, _} <- Integer.parse(year_str),
{month, _} <- Integer.parse(month_str) do
Mebe2.Web.Routes.Utils.render_posts(
&post_getter(year, month, &1, &2),
&renderer(year, month, &1, &2, &3, &4)
)
else
_ -> Mebe2.Web.Routes.Utils.render_404()
end
end
defp post_getter(year, month, first, limit),
do: {
Mebe2.Engine.DB.get_month_posts(year, month, first, limit),
Mebe2.Engine.DB.get_count(:month, {year, month})
}
defp renderer(year, month, response, posts, total, page),
do: Mebe2.Web.Views.Month.render(response, year, month, posts, total, page)
end

37
lib/web/routes/year.ex Normal file
View file

@ -0,0 +1,37 @@
defmodule Mebe2.Web.Routes.Year do
use Raxx.Server
@impl Raxx.Server
def handle_request(%Raxx.Request{path: ["archive", year_str, "p", page]} = _req, _state) do
with {year, _} <- Integer.parse(year_str) do
Mebe2.Web.Routes.Utils.render_posts(
page,
&post_getter(year, &1, &2),
&renderer(year, &1, &2, &3, &4)
)
else
_ -> Mebe2.Web.Routes.Utils.render_404()
end
end
@impl Raxx.Server
def handle_request(%Raxx.Request{path: ["archive", year_str]} = _req, _state) do
with {year, _} <- Integer.parse(year_str) do
Mebe2.Web.Routes.Utils.render_posts(
&post_getter(year, &1, &2),
&renderer(year, &1, &2, &3, &4)
)
else
_ -> Mebe2.Web.Routes.Utils.render_404()
end
end
defp post_getter(year, first, limit),
do: {
Mebe2.Engine.DB.get_year_posts(year, first, limit),
Mebe2.Engine.DB.get_count(:year, year)
}
defp renderer(year, response, posts, total, page),
do: Mebe2.Web.Views.Year.render(response, year, posts, total, page)
end

17
lib/web/views/archives.ex Normal file
View file

@ -0,0 +1,17 @@
defmodule Mebe2.Web.Views.Archives do
use Raxx.View,
template: "archives.html.eex",
arguments: []
@doc """
Get list of tuples {year, month} that have at least one post.
"""
@spec get_archives() :: [{integer, [{integer, integer}]}]
def get_archives() do
Mebe2.Web.Middleware.Archives.get_archives()
|> Enum.chunk_by(fn {year, _} -> year end)
|> Enum.map(fn [{year, _} | _] = list ->
{year, Enum.map(list, fn {_, month} -> month end)}
end)
end
end

View file

@ -0,0 +1,24 @@
<nav class="archives" aria-labelledby="archives-heading">
<%= raw(with [_|_] = years <- get_archives() do %>
<h2 id="archives-heading">Archives</h2>
<ul class="archives-year-list">
<%= raw(for {year, months} <- years do %>
<li>
<a href="<%= Mebe2.Web.Views.Utils.year_path_generator(year).(1) %>">
<%= year %>
</a>
<ul class="archives-month-list">
<%= raw(for month <- months do %>
<li>
<a href="<%= Mebe2.Web.Views.Utils.month_path_generator(year, month).(1) %>">
<%= Mebe2.Web.Views.Utils.format_month(year, month) %>
</a>
</li>
<% end) %>
</ul>
</li>
<% end) %>
</ul>
<% end) %>
</nav>

View file

@ -24,6 +24,9 @@
<main> <main>
<%= __content__ %> <%= __content__ %>
</main> </main>
<%= Mebe2.Web.Views.Archives.html() %>
<footer> <footer>
<p> <p>
© <%= Mebe2.get_conf(:blog_author) %> © <%= Mebe2.get_conf(:blog_author) %>

5
lib/web/views/month.ex Normal file
View file

@ -0,0 +1,5 @@
defmodule Mebe2.Web.Views.Month do
use Mebe2.Web.Views.BaseLayout,
template: "month.html.eex",
arguments: [:year, :month, :posts, :total, :page]
end

View file

@ -0,0 +1,12 @@
<div class="viewing-info">
Viewing posts for <em><%= Mebe2.Web.Views.Utils.format_month(year, month) %></em>.
</div>
<%=
Mebe2.Web.Views.PostList.html(
posts,
total,
page,
Mebe2.Web.Views.Utils.month_path_generator(year, month)
)
%>

View file

@ -17,4 +17,39 @@ defmodule Mebe2.Web.Views.Utils do
dstr = Calendar.Strftime.strftime!(post.datetime, "%Y/%m/%d") dstr = Calendar.Strftime.strftime!(post.datetime, "%Y/%m/%d")
"/#{dstr}/#{post.slug}" "/#{dstr}/#{post.slug}"
end end
@doc """
Format the given year and month into a human readable nice string.
## Examples
iex> Mebe2.Web.Views.Utils.format_month(2014, 12)
"December 2014"
iex> Mebe2.Web.Views.Utils.format_month(2015, 4)
"April 2015"
"""
@spec format_month(integer, integer) :: String.t()
def format_month(year, month) do
Date.from_erl!({year, month, 1})
|> Calendar.Strftime.strftime!("%B %Y")
end
@doc """
Pad the month for path generation.
"""
@spec pad_month(integer) :: String.t()
def pad_month(month), do: Integer.to_string(month) |> String.pad_leading(2, "0")
@doc """
Get path generator for the given year.
"""
@spec year_path_generator(integer) :: (integer -> String.t())
def year_path_generator(year), do: &"/archive/#{year}/p/#{&1}"
@doc """
Get path generator for the given year and month combo.
"""
@spec month_path_generator(integer, integer) :: (integer -> String.t())
def month_path_generator(year, month), do: &"/archive/#{year}/#{pad_month(month)}/p/#{&1}"
end end

5
lib/web/views/year.ex Normal file
View file

@ -0,0 +1,5 @@
defmodule Mebe2.Web.Views.Year do
use Mebe2.Web.Views.BaseLayout,
template: "year.html.eex",
arguments: [:year, :posts, :total, :page]
end

View file

@ -0,0 +1,12 @@
<div class="viewing-info">
Viewing posts for <em><%= year %></em>.
</div>
<%=
Mebe2.Web.Views.PostList.html(
posts,
total,
page,
Mebe2.Web.Views.Utils.year_path_generator(year)
)
%>

View file

@ -27,6 +27,7 @@ defmodule Mebe2.MixProject do
{:ace, "~> 0.17.1"}, {:ace, "~> 0.17.1"},
{:calendar, "~> 0.17.4"}, {:calendar, "~> 0.17.4"},
{:slugger, "~> 0.3.0"}, {:slugger, "~> 0.3.0"},
{:ex2ms, "~> 1.5.0"},
{:mbu, "~> 3.0.0", runtime: false}, {:mbu, "~> 3.0.0", runtime: false},
{:exsync, "~> 0.2.3", only: :dev} {:exsync, "~> 0.2.3", only: :dev}
] ]

View file

@ -7,6 +7,7 @@
"cowlib": {:hex, :cowlib, "2.3.0", "bbd58ef537904e4f7c1dd62e6aa8bc831c8183ce4efa9bd1150164fe15be4caa", [:rebar3], [], "hexpm"}, "cowlib": {:hex, :cowlib, "2.3.0", "bbd58ef537904e4f7c1dd62e6aa8bc831c8183ce4efa9bd1150164fe15be4caa", [:rebar3], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"eex_html": {:hex, :eex_html, "0.1.1", "df7ad68245068d5fea0dab2a38e5afdc386ec00a5b6445eac6402ed7aa6a2b12", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "eex_html": {:hex, :eex_html, "0.1.1", "df7ad68245068d5fea0dab2a38e5afdc386ec00a5b6445eac6402ed7aa6a2b12", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
"exsync": {:hex, :exsync, "0.2.3", "a1ac11b4bd3808706003dbe587902101fcc1387d9fc55e8b10972f13a563dd15", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, "exsync": {:hex, :exsync, "0.2.3", "a1ac11b4bd3808706003dbe587902101fcc1387d9fc55e8b10972f13a563dd15", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},