Merge branch 'f-content' into 'master'

feat: Add basic content management system

See merge request mythic-insight/legendary!5
This commit is contained in:
Robert Prehn 2020-07-22 19:22:44 +00:00
commit 5a629a3860
136 changed files with 2073 additions and 501 deletions

View file

@ -1,9 +1,15 @@
image: "elixir:1.10"
cache:
paths:
- _build
- deps
variables:
POSTGRES_PASSWORD: "postgres"
POSTGRES_USER: "postgres"
DATABASE_URL: "postgres"
MIX_ENV: "test"
services:
- name: postgres:12

View file

@ -1,4 +1,4 @@
defmodule Auth.Users.User do
defmodule Auth.User do
@moduledoc """
The baseline user schema module.
"""
@ -13,6 +13,8 @@ defmodule Auth.Users.User do
schema "users" do
field :roles, {:array, :string}
field :display_name, :string
field :homepage_url, :string
pow_user_fields()

View file

@ -4,7 +4,7 @@ defmodule Mix.Tasks.Legendary.CreateAdmin do
"""
use Mix.Task
alias Auth.Users.User
alias Auth.User
alias Auth.Repo
alias Ecto.Changeset

View file

@ -0,0 +1,9 @@
defmodule Auth.Repo.Migrations.AddNicenameToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add :display_name, :string, default: nil
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Auth.Repo.Migrations.AddUrlToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add :homepage_url, :string, default: nil
end
end
end

View file

@ -20,7 +20,7 @@ erl_crash.dump
*.ez
# Ignore package tarball (built via "mix hex.build").
auth_web-*.tar
content-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -18,7 +18,7 @@ config :logger, :console,
config :content, Content.Scheduler,
jobs: [
{"@hourly", {ContentWeb.Sitemaps, :generate, []}}
{"@hourly", {Content.Sitemaps, :generate, []}}
]
# Import environment specific config. This must remain at the bottom

View file

@ -4,7 +4,7 @@ use Mix.Config
config :content, Content.Repo,
username: "postgres",
password: "postgres",
database: "content_dev",
database: "legendary_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
@ -15,7 +15,7 @@ config :content, Content.Repo,
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
config :content, ContentWeb.Endpoint,
config :content, Content.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
@ -55,7 +55,7 @@ config :content, ContentWeb.Endpoint,
# different ports.
# Watch static and templates for browser reloading.
config :content, ContentWeb.Endpoint,
config :content, Content.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",

View file

@ -2,11 +2,3 @@ use Mix.Config
# Print only warnings and errors during test
config :logger, level: :warn
config :content, Content.Repo,
username: "postgres",
password: "postgres",
database: "content_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: System.get_env("DATABASE_URL") || "localhost",
show_sensitive_data_on_connection_error: true,
pool: Ecto.Adapters.SQL.Sandbox

View file

@ -1,6 +1,6 @@
defmodule Content.Application do
@moduledoc """
The base module of the CMS application.
The base module of the Content application.
"""
use Application
@ -17,6 +17,8 @@ defmodule Content.Application do
# Start your own worker by calling: Content.Worker.start_link(arg1, arg2, arg3)
# worker(Content.Worker, [arg1, arg2, arg3]),
worker(Content.Scheduler, []),
Content.Telemetry,
Content.Endpoint,
]
# See https://hexdocs.pm/elixir/Supervisor.html
@ -28,7 +30,7 @@ defmodule Content.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
Endpoint.config_change(changed, removed)
Content.Endpoint.config_change(changed, removed)
:ok
end
end

View file

@ -37,7 +37,7 @@ defmodule Content.Post do
has_many :categories, through: [:term_relationships, :category, :term]
has_many :tags, through: [:term_relationships, :tag, :term]
has_one :post_format, through: [:term_relationships, :post_format, :term]
belongs_to :author, Content.User, foreign_key: :post_author, references: :ID
belongs_to :author, Auth.User, foreign_key: :post_author
end
def changeset(struct, params \\ %{}) do
@ -123,14 +123,14 @@ defmodule Content.Post do
end
def maybe_put_guid(changeset) do
import ContentWeb.Router.Helpers, only: [posts_url: 3]
import Content.Router.Helpers, only: [posts_url: 3]
slug = changeset |> get_field(:post_name)
case slug do
nil -> changeset
_ ->
changeset
|> put_default(:guid, posts_url(ContentWeb.Endpoint, :show, slug))
|> put_default(:guid, posts_url(CoreWeb.Endpoint, :show, slug))
end
end
end

View file

@ -159,6 +159,18 @@ defmodule Content.Posts do
"""
def get_posts!(slug) do
slug
|> get_post_scope()
|> Repo.one!()
end
def get_post(slug) do
slug
|> get_post_scope()
|> Repo.one()
end
defp get_post_scope(slug) do
id_filter = fn scope, id ->
case Integer.parse(id, 10) do
@ -172,7 +184,6 @@ defmodule Content.Posts do
post_scope()
|> where([p], p.post_type != "nav_menu_item")
|> id_filter.(slug)
|> Repo.one!()
end
@doc """

View file

@ -1,12 +1,12 @@
defmodule ContentWeb do
defmodule Content do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
use ContentWeb, :controller
use ContentWeb, :view
use Content, :controller
use Content, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
@ -19,11 +19,11 @@ defmodule ContentWeb do
def controller do
quote do
use Phoenix.Controller, namespace: ContentWeb
use Phoenix.Controller, namespace: Content
import Plug.Conn
import ContentWeb.Gettext
alias ContentWeb.Router.Helpers, as: Routes
import Content.Gettext
alias Content.Router.Helpers, as: Routes
end
end
@ -31,13 +31,21 @@ defmodule ContentWeb do
quote do
use Phoenix.View,
root: "lib/content_web/templates",
namespace: ContentWeb,
namespace: Content,
pattern: "**/*"
use PhoenixHtmlSanitizer, :basic_html
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
def process_content(text) do
text
|> Content.Shortcodes.expand_shortcodes()
|> Earmark.as_html!()
end
# Include shared imports and aliases for views
unquote(view_helpers())
end
@ -55,7 +63,7 @@ defmodule ContentWeb do
def channel do
quote do
use Phoenix.Channel
import ContentWeb.Gettext
import Content.Gettext
end
end
@ -67,9 +75,9 @@ defmodule ContentWeb do
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import ContentWeb.ErrorHelpers
import ContentWeb.Gettext
alias ContentWeb.Router.Helpers, as: Routes
import Content.ErrorHelpers
import Content.Gettext
alias Content.Router.Helpers, as: Routes
end
end

View file

@ -1,8 +1,8 @@
defmodule ContentWeb.UserSocket do
defmodule Content.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", ContentWeb.RoomChannel
# channel "room:*", Content.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
@ -27,7 +27,7 @@ defmodule ContentWeb.UserSocket do
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# ContentWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
# Content.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true

View file

@ -0,0 +1,7 @@
defmodule Content.AdminHomeController do
use Content, :controller
def index(conn, _params) do
render conn, "index.html"
end
end

View file

@ -0,0 +1,27 @@
defmodule Content.AdminPostsController do
use Content, :controller
alias Content.Posts
require Ecto.Query
def index(conn, %{"page" => page, "post_type" => post_type}) do
posts = Posts.list_admin_posts(page, post_type)
thumbs = posts |> Posts.thumbs_for_posts()
last_page = Posts.last_page
render(
conn,
"index.html",
[
posts: posts,
page: String.to_integer(page),
last_page: last_page,
thumbs: thumbs,
post_type: post_type,
]
)
end
def index(conn, %{"page" => page} = params), do: index(conn, params |> Map.merge(%{"page" => page, "post_type" => "post"}))
def index(conn, %{"post_type" => post_type} = params), do: index(conn, params |> Map.merge(%{"page" => "1", "post_type" => post_type}))
def index(conn, params), do: index(conn, params |> Map.merge(%{"page" => "1", "post_type" => "post"}))
end

View file

@ -0,0 +1,71 @@
defmodule Content.CommentController do
use Content, :controller
alias Content
alias Content.Comments
alias Content.Post
alias Content.Repo
import Ecto.Query
def create(conn, %{"comment" => comment_params}) do
comment_params = comment_params |> Map.merge(%{"comment_author_IP" => to_string(:inet_parse.ntoa(conn.remote_ip))})
case Comments.create_comment(comment_params) do
{:ok, comment} ->
post =
Post
|> where([p], p.'ID' == ^comment.comment_post_ID)
|> Repo.one()
conn
|> put_flash(:info, "Comment created successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
{:error, _} ->
post =
Post
|> where([p], p.'ID' == ^comment_params["comment_post_ID"])
|> Repo.one()
conn
|> redirect(to: Routes.posts_path(conn, :show, post))
end
end
def update(conn, %{"id" => id, "comment" => comment_params}) do
comment = Comments.get_comment!(id)
case Comments.update_comment(comment, comment_params) do
{:ok, comment} ->
post =
Post
|> where([p], p.'ID' == ^comment.comment_post_ID)
|> Repo.one()
conn
|> put_flash(:info, "Comment updated successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
{:error, _} ->
post =
Post
|> where([p], p.'ID' == ^comment_params["comment_post_ID"])
|> Repo.one()
conn
|> redirect(to: Routes.posts_path(conn, :show, post))
end
end
def delete(conn, %{"id" => id}) do
comment = Comments.get_comment!(id)
{:ok, comment} = Comments.delete_comment(comment)
post =
Post
|> where([p], p.'ID' == ^comment.comment_post_ID)
|> Repo.one()
conn
|> put_flash(:info, "Comment deleted successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
end
end

View file

@ -0,0 +1,33 @@
defmodule Content.FeedsController do
use Content, :controller
alias Content.{Posts}
plug :put_layout, false when action in [:preview]
def index(conn, params) do
category = params |> Map.get("category")
posts = Posts.list_posts(params)
thumbs = posts |> Posts.thumbs_for_posts()
feed_url =
case category do
nil ->
Routes.index_feed_path(conn, :index)
_ ->
Routes.category_feed_path(conn, :index, category)
end
conn
|> put_resp_content_type("text/xml")
|> render(
"index.rss",
[
posts: posts,
thumbs: thumbs,
category: category,
feed_url: feed_url,
]
)
end
end

View file

@ -0,0 +1,39 @@
defmodule Content.MenusController do
use Content, :controller
alias Content.Repo
def edit(conn, %{"id" => id}) do
menu = id |> Content.Menu.get_menu_from_id()
posts =
Content.Posts.post_scope
|> Repo.all()
|> Enum.map(fn post ->
post |> Map.take([:ID, :post_title, :post_name])
end)
categories =
Content.Terms.categories
|> Repo.all()
|> Enum.map(fn cat ->
cat |> Map.take([:name, :slug, :term_group, :term_id])
end)
conn
|> render(
"edit.html",
[
id: id,
menu: menu,
posts: posts,
categories: categories,
]
)
end
def update(conn, %{"id" => id, "menu" => menu}) do
Content.UpdateMenu.run(id, menu |> Phoenix.json_library().decode!())
conn
|> redirect(to: Routes.menus_path(conn, :edit, id))
end
end

View file

@ -1,5 +1,5 @@
defmodule ContentWeb.PageController do
use ContentWeb, :controller
defmodule Content.PageController do
use Content, :controller
def index(conn, _params) do
render(conn, "index.html")

View file

@ -0,0 +1,20 @@
defmodule Content.PostPasswordController do
use Content, :controller
def create(conn, %{"post_password" => post_password}) do
conn = put_session(conn, "post_password", post_password)
redirect_path =
if Enum.count(get_req_header(conn, "referer")) > 0 do
conn
|> get_req_header("referer")
|> Enum.at(0)
|> URI.parse()
|> (&(&1.path)).()
else
"/"
end
redirect conn, to: redirect_path
end
end

View file

@ -0,0 +1,176 @@
defmodule Content.PostsController do
use Content, :controller
alias Auth.User
alias Content.{Options, Post, Posts, Repo}
plug :put_layout, false when action in [:preview]
def index(conn, params) do
show_on_front = Options.get_value("show_on_front") || "posts"
case show_on_front do
"posts" ->
conn |> index_posts(params)
"page" ->
conn |> index_page(params)
end
end
def index_posts(conn, params) do
page = params |> Map.get("page", "1")
params = params |> Map.merge(%{"page" => page})
category = params |> Map.get("category")
posts = Posts.list_posts(params)
thumbs = posts |> Posts.thumbs_for_posts()
last_page = Posts.last_page(params)
conn
|> render(
"index.html",
[
posts: posts,
page: String.to_integer(page),
last_page: last_page,
thumbs: thumbs,
category: category,
]
)
end
def index_page(conn, _params) do
page_id = Options.get("page_on_front") |> (&(&1.option_value)).()
post = Posts.get_posts!(page_id)
page = String.to_integer("1")
thumbs = [post] |> Posts.thumbs_for_posts()
render(conn, "front.html", post: post, page: page, thumbs: thumbs)
end
def new(conn, params) do
changeset = Posts.change_posts(%Post{})
render(
conn,
"new.html",
changeset: changeset,
post_type: params["post_type"] || "post",
author_options: User |> Repo.all()
)
end
def create(conn, %{"post" => post_params}) do
case Posts.create_posts(post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Posts created successfully.")
|> redirect(to: Routes.posts_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(
conn,
"new.html",
changeset: changeset,
post_type: post_params["post_type"] || "post",
author_options: User |> Repo.all()
)
end
end
def preview(conn, %{"post" => post_params}) do
post = Posts.preview_post(post_params)
conn
|> render("show.html", post: post, page: 1, thumbs: [])
end
def show(conn, %{"id" => id, "page" => page_string}) do
{page_id_for_posts, _} = Options.get_value_as_int("page_for_posts")
post = Posts.get_post(id)
if is_nil(post) do
try_static_post(conn, id)
else
if post.'ID' == page_id_for_posts do
conn |> index_posts(%{"id" => id, "page" => page_string})
else
conn |> show_one(post, page_string)
end
end
end
def show(conn, %{"id" => id}), do: show(conn, %{"id" => id, "page" => "1"})
defp try_static_post(conn, id) do
try do
render(conn, "static_pages/#{id}.html")
rescue
Phoenix.Template.UndefinedError ->
raise Phoenix.Router.NoRouteError.exception(conn: conn, router: Content.Router)
end
end
def show_one(conn, post, page_string) do
{front_page_id, _} = Options.get_value_as_int("page_on_front")
template =
if post.'ID' == front_page_id do
"front.html"
else
"show.html"
end
page = String.to_integer(page_string)
thumbs = [post] |> Posts.thumbs_for_posts()
case post.post_type do
"attachment" ->
{:ok, decoded} = post.post_content |> Base.decode64
conn
|> put_resp_content_type(post.post_mime_type, "binary")
|> send_resp(conn.status || 200, decoded)
_ ->
render(conn, template, post: post, page: page, thumbs: thumbs)
end
end
def edit(conn, %{"id" => id}) do
posts = Posts.get_post_with_drafts!(id)
changeset = Posts.change_posts(posts)
render(
conn,
"edit.html",
posts: posts,
changeset: changeset,
post_type: posts.post_type || "post",
author_options: User |> Repo.all()
)
end
def update(conn, %{"id" => id, "post" => posts_params}) do
posts = Posts.get_post_with_drafts!(id)
case Posts.update_posts(posts, posts_params) do
{:ok, posts} ->
conn
|> put_flash(:info, "Posts updated successfully.")
|> redirect(to: Routes.posts_path(conn, :edit, posts))
{:error, %Ecto.Changeset{} = changeset} ->
render(
conn,
"edit.html",
posts: posts,
changeset: changeset,
post_type: posts.post_type || "post",
author_options: User |> Repo.all()
)
end
end
def delete(conn, %{"id" => id}) do
posts = Posts.get_post_with_drafts!(id)
{:ok, _posts} = Posts.delete_posts(posts)
conn
|> put_flash(:info, "Posts deleted successfully.")
|> redirect(to: Routes.admin_posts_path(conn, :index))
end
end

View file

@ -0,0 +1,20 @@
defmodule Content.SitemapController do
use Content, :controller
alias Content.{Posts, Repo, Terms}
import Ecto.Query
def index(conn, _params) do
posts =
Posts.post_scope
|> where([p], p.post_type not in ["nav_menu_item", "attachment"])
|> Repo.all()
categories =
Terms.categories
|> Repo.all()
render(conn, "index.html", conn: conn, posts: posts, categories: categories)
end
end

View file

@ -1,5 +1,5 @@
defmodule ContentWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :content_web
defmodule Content.Endpoint do
use Phoenix.Endpoint, otp_app: :content
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
@ -10,7 +10,7 @@ defmodule ContentWeb.Endpoint do
signing_salt: "wfYQp84C"
]
socket "/socket", ContentWeb.UserSocket,
socket "/socket", Content.UserSocket,
websocket: true,
longpoll: false
@ -22,7 +22,7 @@ defmodule ContentWeb.Endpoint do
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :content_web,
from: :content,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
@ -32,7 +32,7 @@ defmodule ContentWeb.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :content_web
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :content
end
plug Phoenix.LiveDashboard.RequestLogger,
@ -50,6 +50,6 @@ defmodule ContentWeb.Endpoint do
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug Pow.Plug.Session, otp_app: :content_web
plug ContentWeb.Router
plug Pow.Plug.Session, otp_app: :content
plug Content.Router
end

View file

@ -1,11 +1,11 @@
defmodule ContentWeb.Gettext do
defmodule Content.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import ContentWeb.Gettext
import Content.Gettext
# Simple translation
gettext("Here is the string to translate")
@ -20,5 +20,5 @@ defmodule ContentWeb.Gettext do
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :content_web
use Gettext, otp_app: :content
end

View file

@ -0,0 +1,36 @@
defmodule Content.LoadUser do
@moduledoc """
Loads user into connection if user has session
"""
import Plug.Conn
alias Content.Users
def init(opts) do
opts
end
def call(conn, _) do
handle_auth(conn)
end
defp handle_auth(conn) do
user_id = get_session(conn, :user_id)
if user = (user_id && Users.get_user(user_id)) do
build_conn(conn, user)
else
build_conn(conn, nil)
end
end
defp build_conn(conn, nil) do
conn
|> assign(:current_user, nil)
end
defp build_conn(conn, user) do
conn
|> assign(:current_user, user)
end
end

View file

@ -0,0 +1,22 @@
defmodule Content.RequireAdmin do
@moduledoc """
A plug that returns 403 unauthorized if the user is not an admin. Used
to block out logged-in-only routes.
"""
import Plug.Conn
alias Auth.User
def init(opts) do
opts
end
def call(conn, _opts) do
if conn.assigns[:current_user] && User.is_admin?(conn.assigns[:current_user]) do
conn
else
conn
|> send_resp(403, "Unauthorized")
|> halt()
end
end
end

View file

@ -0,0 +1,22 @@
defmodule Content.RequireAuth do
@moduledoc """
A plug that returns 403 unauthorized if the user is not authenticated. Used
to block out logged-in-only routes.
"""
import Plug.Conn
def init(opts) do
opts
end
def call(conn, _) do
case conn.assigns[:current_user] do
nil ->
conn
|> send_resp(403, "Unauthorized")
|> halt()
_user ->
conn
end
end
end

View file

@ -0,0 +1,72 @@
defmodule Content.Router do
use Content, :router
alias Content.{RequireAdmin, RequireAuth}
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
pipeline :feed do
plug :accepts, ["rss"]
plug :fetch_session
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :require_auth do
plug(RequireAuth)
end
pipeline :require_admin do
plug(RequireAdmin)
end
pipeline :admin_layout do
plug :put_layout, {Content.LayoutView, :admin}
end
scope "/", Content do
pipe_through([:browser, :require_auth, :require_admin, :admin_layout])
get "/wp-admin/", AdminHomeController, :index
get "/posts", AdminPostsController, :index, as: :admin_posts
post "/posts", PostsController, :create
get "/posts/new", PostsController, :new
put "/posts/preview", PostsController, :preview
post "/posts/preview", PostsController, :preview
get "/posts/:id/edit", PostsController, :edit
put "/posts/:id", PostsController, :update
delete "/posts/:id", PostsController, :delete
get "/menus/:id/edit", MenusController, :edit
put "/menus/:id", MenusController, :update
end
scope "/", Content do
pipe_through :feed # Use the default browser stack
get "/category/:category/feed.rss", FeedsController, :index, as: :category_feed
get "/feed.rss", FeedsController, :index, as: :index_feed
end
scope "/", Content do
pipe_through :browser # Use the default browser stack
resources "/comments", CommentController, as: :comment, only: [:create, :delete, :update]
get "/page/:page", PostsController, :index_posts, as: :blog_page
get "/category/:category", PostsController, :index_posts, as: :category
get "/category/:category/page/:page", PostsController, :index, as: :category_page
post "/wp-login.php", PostPasswordController, :create
get "/", PostsController, :index
resources "/sitemap", SitemapController, only: [:index]
get "/:id", PostsController, :show
get "/:id/:page", PostsController, :show, as: :paged_post
end
end

View file

@ -1,4 +1,4 @@
defmodule ContentWeb.Telemetry do
defmodule Content.Telemetry do
@moduledoc """
Collects metrics for the application and allows them to be transmitted using the Telemetry framework.
"""
@ -34,11 +34,11 @@ defmodule ContentWeb.Telemetry do
),
# Database Metrics
summary("content_web.repo.query.total_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.decode_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.query_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.queue_time", unit: {:native, :millisecond}),
summary("content_web.repo.query.idle_time", unit: {:native, :millisecond}),
summary("content.repo.query.total_time", unit: {:native, :millisecond}),
summary("content.repo.query.decode_time", unit: {:native, :millisecond}),
summary("content.repo.query.query_time", unit: {:native, :millisecond}),
summary("content.repo.query.queue_time", unit: {:native, :millisecond}),
summary("content.repo.query.idle_time", unit: {:native, :millisecond}),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
@ -52,7 +52,7 @@ defmodule ContentWeb.Telemetry do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {ContentWeb, :count_users, []}
# {Content, :count_users, []}
]
end
end

View file

@ -0,0 +1 @@
<h1>Dashboard</h1>

View file

@ -0,0 +1,93 @@
<div class="grid">
<div class="column">
<h1>
<%= humanize(@post_type) %>s
</h1>
</div>
<div class="column admin-actions">
<%= link "New " <> humanize(@post_type), to: Routes.posts_path(@conn, :new, post_type: @post_type), class: "button" %>
</div>
</div>
<table class="admin-table small">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Categories</th>
<th>Tags</th>
<th>Comments</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<%= for post <- @posts do %>
<tr>
<td>
<%= link post.post_title, to: Routes.posts_path(@conn, :edit, post) %>
</td>
<td>
<%= if !is_nil(post.author) do %>
<%= post.author.display_name %>
<% end %>
</td>
<td>
<%= post.categories |> Enum.map(&(&1.name)) |> Enum.join(", ") %>
</td>
<td>
<%= post.tags |> Enum.map(&(&1.name)) |> Enum.join(", ") %>
</td>
<td>
<%= post.comments |> Enum.count() %>
</td>
<td>
<%= case post.post_status do %>
<% "publish" -> %>
<%= "Published" %>
<% "future" -> %>
<%= "Scheduled" %>
<% "draft" -> %>
<%= "Last Modified" %>
<% "pending" -> %>
<%= "Scheduled" %>
<% "private" -> %>
<%= "Published Privately" %>
<% "inherit" -> %>
<%= "Inherit" %>
<% end %>
<div>
<%= post.post_date |> Timex.format!("%F", :strftime) %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<hr />
<%= if @page > 1 do %>
<%= link 1, to: Routes.admin_posts_path(@conn, :index, page: 1) %>
<% end %>
<%= if @page > 3 do %>
...
<% end %>
<%= if @page > 2 do %>
<%= link @page - 1, to: Routes.admin_posts_path(@conn, :index, page: @page - 1) %>
<% end %>
<%= @page %>
<%= if @page + 1 < @last_page do %>
<%= link @page + 1, to: Routes.admin_posts_path(@conn, :index, page: @page + 1) %>
<% end %>
<%= if @page + 2 < @last_page do %>
...
<% end %>
<%= if @page < @last_page do %>
<%= link @last_page, to: Routes.admin_posts_path(@conn, :index, page: @last_page) %>
<% end %>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title><%= LayoutView.title(@view_module, @view_template, assigns) %></title>
<description><%= LayoutView.excerpt(@view_module, @view_template, assigns) %></description>
<link>https://pre.hn</link>
<atom:link href="https://pre.hn<%= @feed_url %>" rel="self" type="application/rss+xml" />
<%= for post <- @posts do %>
<item>
<title><%= post.post_title |> HtmlSanitizeEx.strip_tags() %></title>
<description>
<%= post.post_content |> process_content |> html_escape |> safe_to_string %>
</description>
<pubDate><%=
post.post_date
|> DateTime.from_naive!("Etc/UTC")
|> Timex.format!("{WDshort}, {D} {Mshort} {YYYY} {h24}:{m}:{s} {Z}")
%></pubDate>
<guid isPermaLink="true"><%= post.guid %></guid>
</item>
<% end %>
</channel>
</rss>

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title><%= title(@view_module, @view_template, assigns) %></title>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>">
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/admin.css") %>">
</head>
<body>
<div class="container">
<main role="main" class="grid">
<nav class="column admin-nav">
<menu type="list">
<li>
<%= link "🏠 Home", to: Routes.admin_home_path(@conn, :index) %>
</li>
<li>
<%= link "📝 Posts", to: Routes.admin_posts_path(@conn, :index) %>
</li>
<li>
<%= link "📄 Pages", to: Routes.admin_posts_path(@conn, :index, post_type: "page") %>
</li>
<li>
<%= link "👋 Logout", to: AuthWeb.Router.Helpers.pow_session_path(@conn, :delete), method: :delete %>
</li>
</menu>
</nav>
<section class="column four with-gutters ">
<%= if get_flash(@conn, :info) do %>
<input type="checkbox" id="dismiss-alert-info" class="alert-dismisser" />
<div class="alert alert-info" role="alert">
<p class="alert-text">
<%= get_flash(@conn, :info) %>
<label for="dismiss-alert-info">Dismiss</label>
</p>
</div>
<% end %>
<%= if get_flash(@conn, :error) do %>
<input type="checkbox" id="dismiss-alert-error" class="alert-dismisser" />
<div class="alert alert-error" role="alert">
<p class="alert-text">
<%= get_flash(@conn, :error) %>
<label for="dismiss-alert-error">Dismiss</label>
</p>
</div>
<% end %>
<%= @inner_content %>
</section>
</main>
</div> <!-- /container -->
<script src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
<script src="<%= Routes.static_path(@conn, "/js/admin.js") %>"></script>
</body>
</html>

View file

@ -0,0 +1,5 @@
<div data-react-component="MenuEditor" data-react-props="<%= %{menu: @menu, posts: @posts, categories: @categories, id: @id} |> Phoenix.json_library().encode!() |> Base.encode64() %>">
</div>
<%= form_for @conn, Routes.menus_path(@conn, :update, @id), [method: :put], fn _f -> %>
<% end %>

View file

@ -0,0 +1,35 @@
<%= Enum.map(Content.Comments.children(@parent_id, @post.comments), fn comment -> %>
<li class="Comment">
<div class="Comment-topmatter">
<span class="Gravatar" style="background-image: url(<%= comment.comment_author_email |> gravatar_url_for_email %>)">
</span>
<h4>
<%= comment.comment_author || "Anonymous" %>
</h4>
<h5>
<%= comment.comment_date |> Timex.format!("%F", :strftime) %>
</h5>
</div>
<div class="Comment-content">
<%= sanitize comment.comment_content |> auto_paragraph_tags |> elem(1) |> IO.iodata_to_binary() %>
<p class="Comment-actions">
<a href="#reply-to-<%= comment.comment_ID %>" class="Comment--replyLink">
&#x27A6; Reply
</a>
</p>
</div>
<ul class="CommentList">
<%= render "comments.html", post: @post, parent_id: comment.comment_ID, conn: @conn %>
<li class="Comment Comment--replyForm" id="reply-to-<%= comment.comment_ID %>">
<h4>
Reply to <%= comment.comment_author || "Anonymous" %>
</h4>
<%=
render "reply_form.html",
comment_changeset: comment_changeset_for_parent(comment),
conn: @conn
%>
</li>
</ul>
</li>
<% end) %>

View file

@ -0,0 +1 @@
<%= render "form.html", Map.put(assigns, :action, Routes.posts_path(@conn, :update, @posts)) %>

View file

@ -0,0 +1,82 @@
<%= form_for @changeset, @action, fn f -> %>
<div class="grid">
<div class="column three">
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<ul>
<%= for {error_key, error} <- @changeset.errors |> Keyword.drop([:post_title, :post_content]) do %>
<%= if error do %>
<li><%= error_key %>: <%= error_tag f, error_key %></li>
<% end %>
<% end %>
</ul>
<% end %>
<%= label f, :post_title do %>
Title
<%= text_input f, :post_title %>
<% end %>
<%= label f, :post_content, "data-react-class": "Editor" do %>
Content
<%= textarea f, :post_content, "data-simplemde": true %>
<% end %>
<div class="grid">
<div class="form-group input-group column uLeft">
<%= submit "Delete", class: "btn btn-warning", form: "deleteForm" %>
</div>
<div class="form-group input-group column">
<%= submit "Save", class: "btn btn-primary" %>
</div>
</div>
</div>
<div class="column post-admin-sidebar">
<%= label f, :post_status, class: "column input-group" do %>
<div>Status</div>
<%= select f, :post_status, [{"Publish", :publish}, {"Draft", :draft}] %>
<% end %>
<%= label f, :post_author, class: "input-group" do %>
<div>Author</div>
<%= select f, :post_author, @author_options |> Enum.map(&({&1.display_name, &1."ID"})) %>
<% end %>
<%= label f, :post_excerpt, class: "input-group" do %>
<div>Excerpt</div>
<%= textarea f, :post_excerpt %>
<% end %>
<%= label f, :sticky, class: "input-group" do %>
<div>Sticky?</div>
<%= checkbox f, :sticky %>
<% end %>
<%= label f, :comment_status, class: "input-group" do %>
<div>Comment Status</div>
<%= select f, :comment_status, ["open", "closed"] %>
<% end %>
<%= label f, :ping_status, class: "input-group" do %>
<div>Ping Status</div>
<%= select f, :ping_status, ["open", "closed"] %>
<% end %>
<%= label f, :post_password, class: "input-group" do %>
<div>Post Password</div>
<%= text_input f, :post_password %>
<% end %>
<%= label f, :post_name, class: "input-group" do %>
<div>Slug</div>
<%= text_input f, :post_name %>
<% end %>
<%= label f, :post_order, class: "input-group" do %>
<div>Post Order</div>
<%= number_input f, :post_order %>
<% end %>
</div>
</div>
<%= hidden_input f, :post_type, value: @post_type %>
<% end %>
<%= if assigns[:posts] do %>
<%= form_for @changeset, Routes.posts_path(@conn, :delete, @posts), [method: :delete, id: "deleteForm"], fn _f -> %>
<% end %>
<% end %>

View file

@ -0,0 +1,15 @@
<article class="<%= post_class(@post) %> h-entry">
<div class="uHidden">
<h1 class="p-name">
<%= link to: Routes.posts_path(@conn, :show, @post), class: "u-url" do %>
<%= raw @post.post_title %>
<% end %>
</h1>
<%= post_topmatter(@conn, @post) %>
</div>
<div class="Article-content <%= if @post.post_format, do: @post.post_format.slug %> e-content">
<%= render "thumb.html", post: @post, thumbs: @thumbs %>
<%= @post |> Content.Post.content_page(@page) |> process_content |> raw %>
</div>
<%= render "pagination.html", conn: @conn, post: @post, current_page: @page %>
</article>

View file

@ -0,0 +1,62 @@
<%= for post <- @posts do %>
<article class="<%= post_class(post) %> h-entry">
<h2 class="entry-title p-name">
<%= link to: Routes.posts_path(@conn, :show, post), class: "u-url" do %>
<%= raw post.post_title %>
<% end %>
</h2>
<%= post_topmatter(@conn, post) %>
<div class="Article-content <%= if post.post_format, do: post.post_format.slug %> e-content">
<%= if authenticated_for_post?(@conn, post) do %>
<%= render "thumb.html", post: post, thumbs: @thumbs %>
<div class="Article-content-words">
<%= raw post |> Content.Post.content_page(1) |> Content.Post.before_more |> process_content |> raw %>
<%= if post.post_content =~ "<!--more-->" do %>
<p>
<%= link "Keep Reading", to: Routes.posts_path(@conn, :show, post) %>
</p>
<% end %>
<%= render "pagination.html", conn: @conn, post: post %>
</div>
<% else %>
<%= render "password_form.html", post: post, conn: @conn %>
<% end %>
</div>
<p class="CategoryBlock">
Categories:
<%= for term <- post.categories do %>
<%= link term.name, to: Routes.category_path(@conn, :index_posts, term.slug), class: "p-category" %>
<% end %>
</p>
<hr />
</article>
<% end %>
<nav class="paginator">
Page:
<%= if @page > 1 do %>
<%= link 1, to: paginated_posts_path(@conn, @category, 1) %>
<% end %>
<%= if @page > 3 do %>
<span class="paginator-page">...</span>
<% end %>
<%= if @page > 2 do %>
<%= link @page - 1, to: paginated_posts_path(@conn, @category, @page - 1) %>
<% end %>
<span class="paginator-page"><%= @page %></span>
<%= if @page + 1 < @last_page do %>
<%= link @page + 1, to: paginated_posts_path(@conn, @category, @page + 1) %>
<% end %>
<%= if @page + 2 < @last_page do %>
<span class="paginator-page">...</span>
<% end %>
<%= if @page < @last_page do %>
<%= link @last_page, to: paginated_posts_path(@conn, @category, @last_page) %>
<% end %>
</nav>

View file

@ -0,0 +1 @@
<%= render "form.html", Map.put(assigns, :action, Routes.posts_path(@conn, :create)) %>

View file

@ -0,0 +1,14 @@
<%= if Content.Post.paginated_post?(@post) do %>
<nav class="paginator">
Page:
<%= Enum.map(
1..Content.Post.content_page_count(@post),
fn page ->
if assigns[:current_page] == nil || assigns[:current_page] != page do
link page, to: Routes.paged_post_path(@conn, :show, @post, page)
else
content_tag :span, page, class: "paginator-page"
end
end) %>
</nav>
<% end %>

View file

@ -0,0 +1,11 @@
<%= form_for @conn, "/wp-login.php?action=postpass", [class: "post-password-form"], fn f -> %>
<p>This content is password protected. To view it please enter your password below:</p>
<p>
<%= label f, :post_password do %>
Password: <input name="post_password" id="pwbox-131" type="password" size="20">
<% end %>
</p>
<p class="form-group">
<input type="submit" name="Submit" value="Enter">
</p>
<% end %>

View file

@ -0,0 +1,24 @@
<%= form_for @comment_changeset, Routes.comment_path(@conn, :create), fn f -> %>
<%= hidden_input f, :comment_parent %>
<%= hidden_input f, :comment_post_ID %>
<%= label f, :comment_author do %>
Your Name
<%= text_input f, :comment_author %>
<% end %>
<%= label f, :comment_author_email do %>
Your Email
<%= email_input f, :comment_author_email %>
<% end %>
<%= label f, :comment_author_url do %>
Your Website
<%= text_input f, :comment_author_url %>
<% end %>
<%= label f, :comment_content do %>
Comment
<%= textarea f, :comment_content %>
<% end %>
<div class="form-group">
<%= submit "\u27A6 Reply", class: "btn btn-primary" %>
</div>
<% end %>

View file

@ -0,0 +1,39 @@
<article class="<%= post_class(@post) %> h-entry">
<div>
<h1 class="p-name">
<%= link to: Routes.posts_path(@conn, :show, @post), class: "u-url" do %>
<%= raw @post.post_title %>
<% end %>
</h1>
<%= post_topmatter(@conn, @post) %>
</div>
<div class="Article-content <%= if @post.post_format, do: @post.post_format.slug %> e-content">
<%= render "thumb.html", post: @post, thumbs: @thumbs %>
<%= @post |> Content.Post.content_page(@page) |> process_content |> raw %>
<p class="CategoryBlock">
Categories:
<%= for term <- @post.categories do %>
<%= link term.name, to: Routes.category_path(@conn, :index_posts, term.slug), class: "p-category" %>
<% end %>
</p>
</div>
<%= render "pagination.html", conn: @conn, post: @post, current_page: @page %>
<%= if @post.comment_status == "open" do %>
<h3>Comments</h3>
<a href="#reply-to-post">
Reply to "<%= @post.post_title %>"
</a>
<ul class="CommentList">
<%= render "comments.html", post: @post, parent_id: 0, conn: @conn %>
<li class="Comment" id="reply-to-post">
<%=
render "reply_form.html",
comment_changeset: comment_changeset_for_post(@post),
post: @post,
conn: @conn
%>
</li>
</ul>
<% end %>
<hr />
</article>

View file

@ -0,0 +1,13 @@
<%= case @thumbs[@post.'ID'] do %>
<% thumb = %Content.Post{} -> %>
<%= if thumb |> Content.Attachment.vertical?() do %>
<div class="post-thumbnail post-thumbnail--vertical">
<%= img_tag thumb.guid %>
</div>
<% else %>
<div class="post-thumbnail">
<%= img_tag thumb.guid %>
</div>
<% end %>
<% nil -> %>
<% end %>

View file

@ -0,0 +1,21 @@
<h1>Site Index</h1>
<h2>Posts and Pages</h2>
<ul>
<%= for post <- @posts do %>
<li>
<%= link to: Routes.posts_path(@conn, :show, post) do %>
<%= raw post.post_title %>
<% end %>
</li>
<% end %>
</ul>
<h2>Categories</h2>
<ul>
<%= for category <- @categories do %>
<li>
<%= link to: Routes.category_path(@conn, :index_posts, category.slug)do %>
<%= raw category.name %>
<% end %>
</li>
<% end %>
</ul>

View file

@ -0,0 +1,5 @@
<h2>Edit comment</h2>
<%= render "form.html", Map.put(assigns, :action, Routes.comment_path(@conn, :update, @comment)) %>
<span><%= link "Back", to: Routes.comment_path(@conn, :index) %></span>

View file

@ -0,0 +1,11 @@
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-group">
<%= submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>

View file

@ -0,0 +1,24 @@
<h2>Listing comments</h2>
<table class="table">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<%= for comment <- @comments do %>
<tr>
<td class="text-right">
<span><%= link "Show", to: Routes.comment_path(@conn, :show, comment), class: "btn btn-default btn-xs" %></span>
<span><%= link "Edit", to: Routes.comment_path(@conn, :edit, comment), class: "btn btn-default btn-xs" %></span>
<span><%= link "Delete", to: Routes.comment_path(@conn, :delete, comment), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= link "New comment", to: Routes.comment_path(@conn, :new) %></span>

View file

@ -0,0 +1,5 @@
<h2>New comment</h2>
<%= render "form.html", Map.put(assigns, :action, Routes.comment_path(@conn, :create)) %>
<span><%= link "Back", to: Routes.comment_path(@conn, :index) %></span>

View file

@ -0,0 +1,8 @@
<h2>Show comment</h2>
<ul>
</ul>
<span><%= link "Edit", to: Routes.comment_path(@conn, :edit, @comment) %></span>
<span><%= link "Back", to: Routes.comment_path(@conn, :index) %></span>

View file

@ -0,0 +1,3 @@
defmodule Content.AdminHomeView do
use Content, :view
end

View file

@ -0,0 +1,3 @@
defmodule Content.AdminPostsView do
use Content, :view
end

View file

@ -1,4 +1,4 @@
defmodule ContentWeb.ErrorHelpers do
defmodule Content.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
@ -39,9 +39,9 @@ defmodule ContentWeb.ErrorHelpers do
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(ContentWeb.Gettext, "errors", msg, msg, count, opts)
Gettext.dngettext(Content.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(ContentWeb.Gettext, "errors", msg, opts)
Gettext.dgettext(Content.Gettext, "errors", msg, opts)
end
end
end

View file

@ -1,5 +1,5 @@
defmodule ContentWeb.ErrorView do
use ContentWeb, :view
defmodule Content.ErrorView do
use Content, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.

View file

@ -0,0 +1,70 @@
defmodule Content.FeedsView do
use Content, :view
use Phoenix.HTML
alias Phoenix.HTML
alias Phoenix.HTML.Tag
alias Content.LayoutView
def gravatar_url_for_email(email) do
email
|> Kernel.||("noreply@example.com")
|> String.trim()
|> String.downcase()
|> (&(:crypto.hash(:md5, &1))).()
|> Base.encode16()
|> String.downcase()
|> (&("https://www.gravatar.com/avatar/#{&1}")).()
end
def auto_paragraph_tags(string) do
string
|> Kernel.||("")
|> String.split(["\n\n", "\r\n\r\n"], trim: true)
|> Enum.map(fn text ->
[Tag.content_tag(:p, text |> HTML.raw(), []), ?\n]
end)
|> HTML.html_escape()
end
def post_class(post) do
sticky =
if post.sticky do
"sticky"
end
"post post-#{post.'ID'} #{sticky}"
end
def post_topmatter(conn, post) do
author =
post.author ||
%Auth.User{
email: "example@example.org",
display_name: "Anonymous",
homepage_url: "#"
}
assigns = %{post: post, author: author, conn: conn}
~E"""
<% _ = assigns # suppress unused assigns warning %>
<div class="Comment-topmatter">
<h4>
<%= link to: author.homepage_url, rel: "author", class: "p-author h-card" do %>
<%= author.display_name %>
<%= img_tag gravatar_url_for_email(author.email), alt: "Photo of #{author.display_name}", class: "Gravatar u-photo" %>
<% end %>
</h4>
<h5>
<%= link to: Routes.posts_path(conn, :show, post) do %>
<time class="dt-published" datetime="<%= post.post_date %>">
<%= post.post_date |> Timex.format!("%F", :strftime) %>
</time>
<% end %>
</h5>
</div>
"""
end
def unauthenticated_post?(_conn, post) do
post.post_password == nil || String.length(post.post_password) == 0
end
end

View file

@ -0,0 +1,92 @@
defmodule Content.LayoutView do
use Content, :view
alias Content.{Option, Options}
def title(Content.PostsView, "index.html", assigns) do
"Page #{assigns.page} | #{title(nil, nil, nil)}"
end
def title(Content.FeedsView, "index.rss", %{category: category}) do
"#{category} | #{title(nil, nil, nil)}"
end
def title(Content.PostsView, "show.html", assigns) do
(assigns.post.post_title |> HtmlSanitizeEx.strip_tags()) <> " | " <> title(nil, nil, nil)
end
def title(_, _, _) do
case Options.get("blogname") do
opt = %Option{} ->
opt.option_value
_ ->
"Hello"
end
end
def excerpt(Content.PostsView, "show.html", assigns) do
assigns.post.post_excerpt
|> HtmlSanitizeEx.strip_tags()
end
def excerpt(Content.FeedsView, "index.rss", %{category: category}) do
"#{category} | #{excerpt(nil, nil, nil)}"
end
def excerpt(_, _, _) do
case Options.get("blogdescription") do
opt = %Option{} ->
opt.option_value
_ ->
"Yet another website"
end
end
def author(Content.PostsView, "show.html", assigns) do
case assigns do
%{author: %{display_name: name}} ->
name
_ ->
"Anonymous"
end
end
def author(_, _, _) do
"Anonymous"
end
def corresponding_feed_url(conn, _, _, %{category: nil}) do
Routes.index_feed_url(conn, :index)
end
def corresponding_feed_url(conn, Content.PostsView, "index.html", %{category: category}) do
Routes.category_feed_url(conn, :index, category)
end
def corresponding_feed_url(conn, _, _, _) do
Routes.index_feed_url(conn, :index)
end
def menu_markup(menu_items, conn), do: menu_markup(menu_items, conn, 0)
def menu_markup(nil, _, _), do: ""
def menu_markup([], _, _), do: ""
def menu_markup(menu_items, conn, level) do
~E"""
<ul style="--menu-level: <%= level %>;">
<%= for item <- menu_items do %>
<li>
<label>
<%= case item[:type] do %>
<% "category" -> %>
<%= link item[:related_item].title, to: Routes.category_path(conn, :index_posts, item[:related_item].slug) %>
<% _ -> %>
<%= link item[:related_item].title, to: Routes.posts_path(conn, :show, item[:related_item].slug) %>
<% end %>
<%= menu_markup(item[:children], conn, level + 1) %>
</label>
</li>
<% end %>
</ul>
"""
end
end

View file

@ -0,0 +1,3 @@
defmodule Content.MenusView do
use Content, :view
end

Some files were not shown because too many files have changed in this diff Show more