From f21e45976046c3a9026129576ac22561823de085 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sat, 3 Nov 2018 15:21:40 +0200 Subject: [PATCH] Implement archives and sidebar --- lib/engine/db.ex | 20 +++++++++- lib/engine/slug_utils.ex | 2 + lib/web/frontend/src/style/archives.scss | 24 ++++++++++++ lib/web/frontend/src/style/base-layout.scss | 13 +++++-- lib/web/frontend/src/style/index.scss | 1 + lib/web/middleware/archives.ex | 42 +++++++++++++++++++++ lib/web/middleware/request_time.ex | 24 ------------ lib/web/router.ex | 5 +++ lib/web/routes/month.ex | 42 +++++++++++++++++++++ lib/web/routes/year.ex | 37 ++++++++++++++++++ lib/web/views/archives.ex | 17 +++++++++ lib/web/views/archives.html.eex | 24 ++++++++++++ lib/web/views/base_layout.html.eex | 3 ++ lib/web/views/month.ex | 5 +++ lib/web/views/month.html.eex | 12 ++++++ lib/web/views/utils.ex | 35 +++++++++++++++++ lib/web/views/year.ex | 5 +++ lib/web/views/year.html.eex | 12 ++++++ mix.exs | 1 + mix.lock | 1 + 20 files changed, 296 insertions(+), 29 deletions(-) create mode 100644 lib/web/frontend/src/style/archives.scss create mode 100644 lib/web/middleware/archives.ex create mode 100644 lib/web/routes/month.ex create mode 100644 lib/web/routes/year.ex create mode 100644 lib/web/views/archives.ex create mode 100644 lib/web/views/archives.html.eex create mode 100644 lib/web/views/month.ex create mode 100644 lib/web/views/month.html.eex create mode 100644 lib/web/views/year.ex create mode 100644 lib/web/views/year.html.eex diff --git a/lib/engine/db.ex b/lib/engine/db.ex index 10a004f..f417c61 100644 --- a/lib/engine/db.ex +++ b/lib/engine/db.ex @@ -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 diff --git a/lib/engine/slug_utils.ex b/lib/engine/slug_utils.ex index 64deba1..438cb22 100644 --- a/lib/engine/slug_utils.ex +++ b/lib/engine/slug_utils.ex @@ -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 diff --git a/lib/web/frontend/src/style/archives.scss b/lib/web/frontend/src/style/archives.scss new file mode 100644 index 0000000..fecc64d --- /dev/null +++ b/lib/web/frontend/src/style/archives.scss @@ -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; + } + } +} diff --git a/lib/web/frontend/src/style/base-layout.scss b/lib/web/frontend/src/style/base-layout.scss index 884322c..cd47e3e 100644 --- a/lib/web/frontend/src/style/base-layout.scss +++ b/lib/web/frontend/src/style/base-layout.scss @@ -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; diff --git a/lib/web/frontend/src/style/index.scss b/lib/web/frontend/src/style/index.scss index b394a83..bacb624 100644 --- a/lib/web/frontend/src/style/index.scss +++ b/lib/web/frontend/src/style/index.scss @@ -13,6 +13,7 @@ @import './typography'; @import './media'; @import './base-layout'; +@import './archives'; @import './post-list'; @import './pagination'; @import './post-layout'; diff --git a/lib/web/middleware/archives.ex b/lib/web/middleware/archives.ex new file mode 100644 index 0000000..2b6acd2 --- /dev/null +++ b/lib/web/middleware/archives.ex @@ -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 diff --git a/lib/web/middleware/request_time.ex b/lib/web/middleware/request_time.ex index b90783d..4f1e71e 100644 --- a/lib/web/middleware/request_time.ex +++ b/lib/web/middleware/request_time.ex @@ -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 diff --git a/lib/web/router.ex b/lib/web/router.ex index a0b5af0..b2a6f7a 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -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}, diff --git a/lib/web/routes/month.ex b/lib/web/routes/month.ex new file mode 100644 index 0000000..a516eec --- /dev/null +++ b/lib/web/routes/month.ex @@ -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 diff --git a/lib/web/routes/year.ex b/lib/web/routes/year.ex new file mode 100644 index 0000000..5111b6a --- /dev/null +++ b/lib/web/routes/year.ex @@ -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 diff --git a/lib/web/views/archives.ex b/lib/web/views/archives.ex new file mode 100644 index 0000000..8129093 --- /dev/null +++ b/lib/web/views/archives.ex @@ -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 diff --git a/lib/web/views/archives.html.eex b/lib/web/views/archives.html.eex new file mode 100644 index 0000000..0a928e5 --- /dev/null +++ b/lib/web/views/archives.html.eex @@ -0,0 +1,24 @@ + diff --git a/lib/web/views/base_layout.html.eex b/lib/web/views/base_layout.html.eex index 27ce278..d6edc0c 100644 --- a/lib/web/views/base_layout.html.eex +++ b/lib/web/views/base_layout.html.eex @@ -24,6 +24,9 @@
<%= __content__ %>
+ + <%= Mebe2.Web.Views.Archives.html() %> +