Implement archives and sidebar
This commit is contained in:
parent
ca62b6fe41
commit
f21e459760
20 changed files with 296 additions and 29 deletions
|
@ -1,5 +1,6 @@
|
|||
defmodule Mebe2.Engine.DB do
|
||||
require Logger
|
||||
import Ex2ms
|
||||
alias Mebe2.Engine.{Utils, SlugUtils, Models}
|
||||
|
||||
alias Calendar.DateTime
|
||||
|
@ -188,6 +189,20 @@ defmodule Mebe2.Engine.DB do
|
|||
get_post_list(@post_table, [{{{year, month, :_, :_}, :"$1"}, [], [:"$_"]}], first, limit)
|
||||
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
|
||||
def get_page(slug) do
|
||||
case :ets.match_object(@page_table, {slug, :"$1"}) do
|
||||
|
@ -209,7 +224,7 @@ defmodule Mebe2.Engine.DB do
|
|||
get_count(:all, :all)
|
||||
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
|
||||
get_meta(type, key, 0)
|
||||
end
|
||||
|
@ -224,7 +239,8 @@ defmodule Mebe2.Engine.DB do
|
|||
:ets.insert(@meta_table, {{type, key}, value})
|
||||
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
|
||||
case :ets.match_object(@meta_table, {{type, key}, :"$1"}) do
|
||||
[{{_, _}, value}] -> value
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Mebe2.Engine.SlugUtils do
|
|||
|
||||
Nil is returned as is.
|
||||
"""
|
||||
@spec slugify(String.t() | nil) :: String.t() | nil
|
||||
def slugify(nil), do: nil
|
||||
|
||||
def slugify(value) do
|
||||
|
@ -19,6 +20,7 @@ defmodule Mebe2.Engine.SlugUtils do
|
|||
@doc """
|
||||
Get the author name related to this slug from the DB.
|
||||
"""
|
||||
@spec unslugify_author(String.t()) :: String.t()
|
||||
def unslugify_author(slug) do
|
||||
DB.get_author_name(slug)
|
||||
end
|
||||
|
|
24
lib/web/frontend/src/style/archives.scss
Normal file
24
lib/web/frontend/src/style/archives.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,14 +8,15 @@ html {
|
|||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template: 'hd' 'mn' 1fr 'ft' / auto;
|
||||
grid-template: 'hd' 'mn' 1fr 'nav' 'ft' / auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
@media #{$md} {
|
||||
grid-template: 'hd mn'
|
||||
'hd ft'
|
||||
grid-template: 'hd mn' 200px
|
||||
'nav mn'
|
||||
'nav ft'
|
||||
/ 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 {
|
||||
grid-area: ft;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
@import './typography';
|
||||
@import './media';
|
||||
@import './base-layout';
|
||||
@import './archives';
|
||||
@import './post-list';
|
||||
@import './pagination';
|
||||
@import './post-layout';
|
||||
|
|
42
lib/web/middleware/archives.ex
Normal file
42
lib/web/middleware/archives.ex
Normal 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
|
|
@ -21,30 +21,6 @@ defmodule Mebe2.Web.Middleware.RequestTime do
|
|||
super(head, config)
|
||||
|> unquote(__MODULE__).process_response()
|
||||
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
|
||||
|
||||
|
|
|
@ -2,10 +2,15 @@ defmodule Mebe2.Web.Router do
|
|||
use Ace.HTTP.Service, port: 2142, cleartext: true
|
||||
use Raxx.Logger
|
||||
use Mebe2.Web.Middleware.RequestTime
|
||||
use Mebe2.Web.Middleware.Archives
|
||||
|
||||
use Raxx.Router, [
|
||||
{%{method: :GET, path: ["tag", _tag, "p", _page]}, 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: [_year, _month, _day, _slug]}, Mebe2.Web.Routes.Post},
|
||||
{%{method: :GET, path: [_slug]}, Mebe2.Web.Routes.Page},
|
||||
|
|
42
lib/web/routes/month.ex
Normal file
42
lib/web/routes/month.ex
Normal 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
37
lib/web/routes/year.ex
Normal 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
17
lib/web/views/archives.ex
Normal 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
|
24
lib/web/views/archives.html.eex
Normal file
24
lib/web/views/archives.html.eex
Normal 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>
|
|
@ -24,6 +24,9 @@
|
|||
<main>
|
||||
<%= __content__ %>
|
||||
</main>
|
||||
|
||||
<%= Mebe2.Web.Views.Archives.html() %>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
© <%= Mebe2.get_conf(:blog_author) %>
|
||||
|
|
5
lib/web/views/month.ex
Normal file
5
lib/web/views/month.ex
Normal 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
|
12
lib/web/views/month.html.eex
Normal file
12
lib/web/views/month.html.eex
Normal 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)
|
||||
)
|
||||
%>
|
|
@ -17,4 +17,39 @@ defmodule Mebe2.Web.Views.Utils do
|
|||
dstr = Calendar.Strftime.strftime!(post.datetime, "%Y/%m/%d")
|
||||
"/#{dstr}/#{post.slug}"
|
||||
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
|
||||
|
|
5
lib/web/views/year.ex
Normal file
5
lib/web/views/year.ex
Normal 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
|
12
lib/web/views/year.html.eex
Normal file
12
lib/web/views/year.html.eex
Normal 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)
|
||||
)
|
||||
%>
|
1
mix.exs
1
mix.exs
|
@ -27,6 +27,7 @@ defmodule Mebe2.MixProject do
|
|||
{:ace, "~> 0.17.1"},
|
||||
{:calendar, "~> 0.17.4"},
|
||||
{:slugger, "~> 0.3.0"},
|
||||
{:ex2ms, "~> 1.5.0"},
|
||||
{:mbu, "~> 3.0.0", runtime: false},
|
||||
{:exsync, "~> 0.2.3", only: :dev}
|
||||
]
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -7,6 +7,7 @@
|
|||
"cowlib": {:hex, :cowlib, "2.3.0", "bbd58ef537904e4f7c1dd62e6aa8bc831c8183ce4efa9bd1150164fe15be4caa", [:rebar3], [], "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"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
|
Reference in a new issue