added content, post display

This commit is contained in:
Adam Piontek 2021-04-03 23:22:35 -04:00
parent ae9fff231b
commit a6186ba639
36 changed files with 751 additions and 199 deletions

View file

@ -18,9 +18,16 @@ defmodule Home73k.Blog do
def list_posts, do: @posts
def list_tags, do: @tags
# defmodule NotFoundError do
# defexception [:message, plug_status: 404]
# end
defmodule NotFoundError do
defexception [:message, plug_status: 404]
end
def get_post_by_id!(id) do
case Enum.find(list_posts(), nil, &(&1.id == id)) do
%Post{} = post -> post
nil -> raise NotFoundError, "post with id=#{id} not found"
end
end
# def get_posts_by_tag!(tag) do
# case Enum.filter(list_posts(), &(tag in &1.tags)) do

View file

@ -1,8 +1,8 @@
defmodule Home73k.Blog.Post do
@enforce_keys [:title, :slug, :date, :author, :tags, :lede, :body, :corpus]
defstruct [:title, :slug, :date, :author, :tags, :lede, :body, :corpus]
@enforce_keys [:title, :id, :date, :author, :tags, :lede, :body, :corpus]
defstruct [:title, :id, :date, :author, :tags, :lede, :body, :corpus]
@strip_words ~w(the and are for not but had has was all any too one you his her can that with have this will your from they want been much some very them into which then now get its youll youre)
@strip_words ~w(the and are for not but had has was all any too one you his her can that with have this will your from they want been much some very them into which then now get its youll youre isnt wasnt)
@doc """
The public parse!/1 function begins the post parse process by reading
@ -36,7 +36,7 @@ defmodule Home73k.Blog.Post do
# """
defp parse_frontmatter([fm, md]) do
case parse_frontmatter_string(fm) do
{%{} = parsed_fm, _} -> {set_post_slug(parsed_fm), String.trim(md)}
{%{} = parsed_fm, _} -> {set_post_id(parsed_fm), String.trim(md)}
{:error, _} -> nil
end
end
@ -46,6 +46,11 @@ defmodule Home73k.Blog.Post do
# """ parse_lede/1
# Look for lede/excerpt/summary in content and extract it if present.
# We return updated frontmatter, and content with <!--more--> stripped.
defp parse_lede({%{lede: lede} = fm, md}) do
lede = String.trim(lede) |> Earmark.as_html!()
{Map.put(fm, :lede, lede), md}
end
defp parse_lede({fm, md}) do
{lede, body_md} = String.split(md, "<!--more-->", parts: 2) |> extract_lede()
{Map.put(fm, :lede, lede), String.replace(body_md, "<!--more-->", " ")}
@ -58,6 +63,8 @@ defmodule Home73k.Blog.Post do
# TODO: handle syntax highlighting
defp parse_body({fm, md}) do
Map.put(fm, :body, Earmark.as_html!(md))
# TODO: Earmark.as_ast(md) |> parse_body(fm)
# def parse_body({:ok, ast, _}, fm)
end
defp parse_body(_), do: nil
@ -66,12 +73,15 @@ defmodule Home73k.Blog.Post do
# Create a searchable word list for the post, for live searching
defp build_corpus(%{title: title, lede: lede, body: body, tags: tags} = post_data) do
# initialize corpus string from: title, lede, body, tags
corpus = (tags ++ [title, (lede && lede) || " ", body]) |> Enum.join(" ") |> String.downcase()
# scrub out (but replace with spaces):
# code blocks, html tags, html entities, newlines, forward and back slashes
html_scrub_regex = ~r/(<pre><code(.|\n)*?<\/code><\/pre>)|(<(.|\n)+?>)|(&#(.)+?;)|(&(.)+?;)|\n|\/|\\/
corpus = Regex.replace(html_scrub_regex, corpus, " ")
# grab text only, rejecting HTML
# downcase & scrub line breaks, slashes
corpus =
(tags ++ [title, (lede && lede) || " ", body])
|> Enum.join(" ")
|> Floki.parse_fragment!()
|> Floki.text()
|> String.downcase()
|> String.replace(["\n", "/", "\\", "(", ")", ":", "=", "_", ".", ",", "[", "]"], " ")
# restrict corpus to letters & numbers,
# then split to words (space delim), trimming as we go
@ -108,8 +118,7 @@ defmodule Home73k.Blog.Post do
# """
defp parse_frontmatter_string(fm) do
try do
String.trim_leading(fm, "-")
|> Code.eval_string()
Code.eval_string(fm)
rescue
_ -> {:error, nil}
end
@ -124,22 +133,23 @@ defmodule Home73k.Blog.Post do
defp extract_lede([body]), do: {nil, body}
# """ set_frontmatter_slug
# If no slug in frontmatter, convert title to slug and add to map
# """ set_post_id
# If no id in frontmatter, convert title to id and add to map
# """
defp set_post_slug(%{slug: _} = fm), do: fm
defp set_post_id(%{id: _} = fm), do: fm
defp set_post_slug(%{title: title} = fm) do
Map.put(fm, :slug, parse_title_to_slug(title))
defp set_post_id(%{title: title} = fm) do
Map.put(fm, :id, parse_title_to_id(title))
end
# """ parse_title_to_slug
# Takes a post title and returns a slug cleansed for URI request path
# """ parse_title_to_id
# Takes a post title and returns a id cleansed for URI request path
# """
defp parse_title_to_slug(title) do
title = String.downcase(title)
def parse_title_to_id(title) do
title_text = Floki.parse_fragment!(title) |> Floki.text() |> String.downcase()
Regex.replace(~r/[^a-z0-9 ]/, title, "")
~r/[^a-z0-9 ]/
|> Regex.replace(title_text, "")
|> String.split(" ", trim: true)
|> Stream.reject(&reject_word?/1)
|> Enum.join("-")
@ -147,7 +157,7 @@ defmodule Home73k.Blog.Post do
# """ reject_word?
# Returns true to reject short or common words
# Used by parse_title_to_slug and build_corpus
# Used by parse_title_to_id and build_corpus
# """
defp reject_word?(word), do: String.length(word) < 3 || word in @strip_words
end

View file

@ -5,6 +5,10 @@ defmodule Home73kWeb.HomeController do
render(conn, "index.html")
end
def about(conn, _params) do
render(conn, "about.html", page_title: "About")
end
def resume(conn, _params) do
render(conn, "resume.html", page_title: "Résumé")
end

View file

@ -23,7 +23,7 @@ defmodule Home73kWeb.Endpoint do
plug Plug.Static,
at: "/",
from: :home73k,
gzip: true,
gzip: (Mix.env() not in [:dev, :test]),
only: ~w(css fonts images js favicon.ico robots.txt DF185CEE29A3D443_public_key.asc)
# Code reloading can be explicitly enabled under the

View file

@ -0,0 +1,54 @@
defmodule Home73kWeb.BlogLive do
use Home73kWeb, :live_view
alias Home73k.Blog
@impl true
def mount(_params, _session, socket) do
socket
|> live_okreply()
end
@impl true
def handle_params(_params, _url, socket) do
socket
|> assign(:page_title, "Blog")
|> assign(:posts, Blog.list_posts())
|> live_noreply()
end
# @impl true
# def handle_event("suggest", %{"q" => query}, socket) do
# {:noreply, assign(socket, results: search(query), query: query)}
# end
# @impl true
# def handle_event("search", %{"q" => query}, socket) do
# case search(query) do
# %{^query => vsn} ->
# {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
# _ ->
# {:noreply,
# socket
# |> put_flash(:error, "No dependencies found matching \"#{query}\"")
# |> assign(results: %{}, query: query)}
# end
# end
# defp search(query) do
# if not Home73kWeb.Endpoint.config(:code_reloader) do
# raise "action disabled when not in development"
# end
# for {app, desc, vsn} <- Application.started_applications(),
# app = to_string(app),
# String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
# into: %{},
# do: {app, vsn}
# end
defp format_date(date) do
Calendar.strftime(date, "%B %-d, %Y")
end
end

View file

@ -0,0 +1,41 @@
<main class="container d-flex justify-content-center">
<div class="col-12 col-md-10 col-lg-8 col-xl-7 col-xxl-6 pb-2 mb-4 mt-3">
<%= for post <- @posts do %>
<div class="post border-bottom border-gray pb-4 mb-3">
<h2 class="post-title fs-2 fw-normal mb-2">
<%= live_redirect "#{post.title}", to: Routes.post_path(@socket, :show, post) %>
</h2>
<div class="post-date font-monospace text-gray-500 <%= if length(post.tags) == 0, do: "mb-3" %>">
<%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline"] %>
<%= post.date |> format_date() %>
</div>
<%= if length(post.tags) > 0 do %>
<div class="post-tags fs-6 mb-3">
<%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline"] %>
<%= for {tag, i} <- Enum.with_index(post.tags) do %>
#<%= tag %><%= i < (length(post.tags) - 1) && "," || "" %>
<% end %>
</div>
<% end %>
<div class="post-lede lead">
<%= raw post.lede %>
</div>
<p>
<%= live_redirect raw("Read more&hellip;"), to: Routes.post_path(@socket, :show, post), class: "fs-6" %>
</p>
</div>
<% end %>
</div>
</main>

View file

@ -1,48 +0,0 @@
defmodule Home73kWeb.PageLive do
use Home73kWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
assign(socket, query: "", results: %{}, page_title: "~")
|> put_flash(:success, "Log in was a success. Good for you.")
|> put_flash(:error, "Lorem ipsum dolor sit amet consectetur adipisicing elit.")
|> put_flash(
:info,
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus dolore sunt quia aperiam sint id reprehenderit? Dolore incidunt alias inventore accusantium nulla optio, ducimus eius aliquam hic, pariatur voluptate distinctio."
)
|> put_flash(:warning, "Oh no, there's nothing to worry about!")
|> put_flash(:primary, "Something in the brand color.")}
end
@impl true
def handle_event("suggest", %{"q" => query}, socket) do
{:noreply, assign(socket, results: search(query), query: query)}
end
@impl true
def handle_event("search", %{"q" => query}, socket) do
case search(query) do
%{^query => vsn} ->
{:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
_ ->
{:noreply,
socket
|> put_flash(:error, "No dependencies found matching \"#{query}\"")
|> assign(results: %{}, query: query)}
end
end
defp search(query) do
if not Home73kWeb.Endpoint.config(:code_reloader) do
raise "action disabled when not in development"
end
for {app, desc, vsn} <- Application.started_applications(),
app = to_string(app),
String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
into: %{},
do: {app, vsn}
end
end

View file

@ -1,48 +0,0 @@
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>Peace of mind from prototype to production</p>
<form phx-change="suggest" phx-submit="search">
<input type="text" name="q" value="<%= @query %>" placeholder="Live dependency search" list="results" autocomplete="off"/>
<datalist id="results">
<%= for {app, _vsn} <- @results do %>
<option value="<%= app %>"><%= app %></option>
<% end %>
</datalist>
<button type="submit" phx-disable-with="Searching...">Go to Hexdocs</button>
</form>
</section>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
</li>
</ul>
</article>
<article class="column">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
<li>
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
</li>
</ul>
</article>
</section>

View file

@ -0,0 +1,61 @@
defmodule Home73kWeb.PostLive do
use Home73kWeb, :live_view
alias Home73k.Blog
@impl true
def mount(%{"id" => id}, session, socket) do
# IO.inspect(params, label: "postlive params")
IO.inspect(session, label: "postlive session")
post = Blog.get_post_by_id!(id)
socket
|> assign(:page_title, "Blog \\ #{post.title}")
|> assign(:post, post)
|> live_okreply()
end
# @impl true
# def handle_params(params, _url, socket) do
# socket
# |> assign(:page_title, "Blog")
# |> assign(:posts, Blog.list_posts())
# |> live_noreply()
# end
# @impl true
# def handle_event("suggest", %{"q" => query}, socket) do
# {:noreply, assign(socket, results: search(query), query: query)}
# end
# @impl true
# def handle_event("search", %{"q" => query}, socket) do
# case search(query) do
# %{^query => vsn} ->
# {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
# _ ->
# {:noreply,
# socket
# |> put_flash(:error, "No dependencies found matching \"#{query}\"")
# |> assign(results: %{}, query: query)}
# end
# end
# defp search(query) do
# if not Home73kWeb.Endpoint.config(:code_reloader) do
# raise "action disabled when not in development"
# end
# for {app, desc, vsn} <- Application.started_applications(),
# app = to_string(app),
# String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
# into: %{},
# do: {app, vsn}
# end
defp format_date(date) do
Calendar.strftime(date, "%B %-d, %Y")
end
end

View file

@ -0,0 +1,36 @@
<main class="container d-flex justify-content-center">
<div class="col-12 col-md-10 col-lg-9 col-xl-8 col-xxl-7 pb-2 mb-4 mt-3">
<div class="post border-bottom border-gray pb-4 mb-3">
<h2 class="post-title fs-2 fw-normal mb-2"><%= raw @post.title %></h2>
<div class="post-date font-monospace text-gray-500 <%= if length(@post.tags) == 0, do: "mb-3" %>">
<%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline"] %>
<%= @post.date |> format_date() %>
</div>
<%= if length(@post.tags) > 0 do %>
<div class="post-tags fs-6 mb-3">
<%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline"] %>
<%= for {tag, i} <- Enum.with_index(@post.tags) do %>
#<%= tag %><%= i < (length(@post.tags) - 1) && "," || "" %>
<% end %>
</div>
<% end %>
<div class="post-lede lead">
<%= raw @post.lede %>
</div>
<div class="post-body">
<%= raw @post.body %>
</div>
</div>
</div>
</main>

View file

@ -18,9 +18,12 @@ defmodule Home73kWeb.Router do
pipe_through :browser
get "/", HomeController, :index
get "/about", HomeController, :about
get "/resume", HomeController, :resume
get "/folio", HomeController, :folio
live "/live", PageLive, :index
live "/blog", BlogLive, :index
# live "/blog/page/:page", BlogLive, :older
live "/blog/:id", PostLive, :show
end
# Other scopes may use custom stacks.

View file

@ -0,0 +1,48 @@
<main class="container d-flex justify-content-center">
<div class="col-12 col-md-10 col-lg-8 col-xl-7 col-xxl-6 border-bottom border-gray pb-3 mb-5 mt-3">
<h2 class="fs-2 fw-600 mb-0">About</h2>
<div class="fs-5 font-monospace text-gray-500">Adam Piontek. Human.</div>
<figure class="my-4 border-start border-gray border-5 ps-3 py-2">
<blockquote class="blockquote">
<p>&ldquo;Probabilities collapse. I become increasingly unlikely.&rdquo;</p>
</blockquote>
<figcaption class="blockquote-footer text-gray-400 mb-0">
Robert Charles Wilson, <cite title="Divided by Infinity">Divided by Infinity</cite>
</figcaption>
</figure>
<div class="post-body">
<p>
From Minnesota, via California, now New York. I'm always learning, and I often think most things should slow down a little, though I'm still working on that practice for myself. I've been a tech nerd since my teens in the 1990s, but didn't begin to take it seriously until around 2009.
</p>
<p>
I'm now a Desktop Systems Engineer with a large global law firm, involved in desktop & mobile security, testing, deployment, and elevated user support.
</p>
<p>
I enjoy spending time with my many pets, talking science fiction & politics, going hiking, and tinkering with programming projects. I've written a few things I'm reasonably proud of &mdash; for work, for family, for myself &mdash; including this website!
</p>
<p>
This site is powered by a custom little <%= link "Phoenix", to: "https://phoenixframework.org/" %> project hosted on a <%= link "Linode", to: "https://www.linode.com/" %> VPS, where I also run instances of <%= link "Gitea", to: "https://gitea.io/" %>, <%= link "Miniflux", to: "https://miniflux.app/" %>, <%= link "ZNC", to: "https://znc.in/" %>, <%= link "The Lounge", to: "https://thelounge.chat/" %>, <%= link "Calibre-Web", to: "https://github.com/janeczku/calibre-web" %>, <%= link "BicBucStriim", to: "https://projekte.textmulch.de/bicbucstriim/" %>, a little <%= link "hubot", to: "https://hubot.github.com/" %> for my family's group chat (Discord for now), and some other odds & ends.
</p>
<p>
At home, between a plucky Raspberry Pi 4 running Ubuntu Linux, and a beefier Windows machine, I also run: <%= link "Jellyfin", to: "https://jellyfin.org/" %>, <%= link "Plex", to: "https://www.plex.tv/" %>, and several apps via Docker, including <%= link "audioserve", to: "https://github.com/izderadicka/audioserve" %>, <%= link "Komga", to: "https://komga.org/" %>, <%= link "Unifi Controller", to: "https://github.com/jacobalberty/unifi-docker" %>, and a group of others (like <%= link "transmission-openvpn", to: "https://hub.docker.com/r/haugene/transmission-openvpn/" %> and <%= link "nzbget", to: "https://hub.docker.com/r/linuxserver/nzbget" %> (Arr!))
</p>
<p>
I'm very lucky to have some super amazing people in my life, always reminding me of how little I know, enabling beautiful experiences, and appreciating places and systems I otherwise wouldn't give a second thought to.
</p>
</div>
</div>
</main>

View file

@ -13,11 +13,11 @@
<div class="col-12 col-sm-10 col-md-7 col-lg-6 col-xl-5 col-xxl-4 justify-content-start ms-lg-3">
<h2 class="font-monospace fs-2 fw-600 mb-0">
<h2 class="font-monospace fs-2 fw-500 mb-0">
<%= icon_div @conn, "mdi-account-hard-hat", [class: "icon baseline me-2"] %><span>Working on it!</span>
</h2>
<div class="font-monospace fs-5">I've made some things over the years (like this site!), and at some point I'll highlight some here.</div>
<div class="font-monospace text-gray-200 fs-5">I've made some things over the years (like this site!), and at some point I'll highlight some here.</div>
</div>

View file

@ -13,11 +13,11 @@
<div class="col-auto justify-content-start ms-lg-3">
<h2 class="font-monospace fs-2 fw-600 mb-0">
<h2 class="font-monospace fs-2 fw-500 mb-0">
<%= icon_div @conn, "mdi-account", [class: "icon baseline me-2"] %><span>Adam Piontek</span>
</h2>
<div class="font-monospace fs-5">Desktop Systems Engineer. Human.</div>
<div class="font-monospace text-gray-500 fs-5">Desktop Systems Engineer. Human.</div>
<div id="social-icons" class="mt-1">
<%= for s <- socials(@conn) do %>

View file

@ -1,14 +1,14 @@
<main class="container-fluid h-100 d-flex justify-content-center align-items-center">
<main class="container-fluid d-flex justify-content-center align-items-center">
<div class="d-flex flex-column mt-5">
<div class="col-auto justify-content-start">
<h2 class="font-monospace fs-2 fw-600 mb-0">
<h2 class="font-monospace fs-2 fw-500 mb-0">
<%= icon_div @conn, "mdi-account", [class: "icon baseline me-2"] %><span>Adam Piontek</span>
</h2>
<div class="font-monospace fs-5">Desktop Systems Engineer.</div>
<div class="font-monospace text-gray-500 fs-5">Desktop Systems Engineer.</div>
<div id="social-icons" class="mt-1">
<%= for s <- socials_prof(@conn) do %>
@ -24,21 +24,21 @@
<div class="separator mt-4 mb-2">qualifications</div>
<ul>
<ul class="text-gray-200">
<%= for qualif <- resume_qualifs() do %>
<%= content_tag :li, qualif %>
<% end %>
</ul>
<div class="separator mt-4 mb-2">experience</div>
<div class="separator mt-4 mb-n2">experience</div>
<%= for %{employer: employer, positions: positions} <- resume_experience() do %>
<div class="fs-5 border-bottom border-gray mt"><%= employer%></div>
<div class="fs-5 border-bottom border-gray mt-3 mb-2"><%= employer%></div>
<div class="mb-3">
<%= for position <- positions do %>
<div>
<span><%= position.title %></span>
<span class="text-gray-300">&middot; <%= position.start %> &mdash; <%= position.end %></span>
<span class="text-gray-200"><%= position.title %></span>
<span class="text-gray-500">&middot; <%= position.start %> &mdash; <%= position.end %></span>
</div>
<% end %>
</div>

View file

@ -1,10 +1,10 @@
<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-dark px-1 px-sm-2 px-lg-3 px-xl-4 px-xxl-5 py-3">
<nav class="navbar <%= if navbar_fixed?(@conn), do: "fixed-top" %> navbar-expand-lg navbar-dark bg-dark px-1 px-sm-2 px-lg-3 px-xl-4 px-xxl-5 py-3">
<div class="container-fluid">
<h1 class="my-0 py-0 lh-base">
<%= link to: Routes.home_path(@conn, :index), class: "navbar-brand fs-1 text-secondary" do %>
<%= icon_div @conn, "mdi-desktop-classic", [class: "icon baseline"] %>
<span class="fw-light" style="font-family: Righteous;">\\73k</span>
<span class="fw-light font-brand">\\73k</span>
<% end %>
</h1>
@ -41,7 +41,13 @@
</li>
<li class="nav-item">
<%= link nav_link_opts(@conn, to: Routes.page_path(@conn, :index), class: "nav-link font-monospace fs-6") do %>
<%= link nav_link_opts(@conn, to: Routes.home_path(@conn, :about), class: "nav-link font-monospace fs-6") do %>
<%= icon_div @conn, "mdi-information", [class: "icon baseline"] %><span>\About</span>
<% end %>
</li>
<li class="nav-item">
<%= link nav_link_opts(@conn, to: Routes.blog_path(@conn, :index), class: "nav-link font-monospace fs-6") do %>
<%= icon_div @conn, "mdi-typewriter", [class: "icon baseline"] %><span>\Blog</span>
<% end %>
</li>

View file

@ -1,6 +1,11 @@
defmodule Home73kWeb.LayoutView do
use Home73kWeb, :view
def navbar_fixed?(conn) do
[Routes.home_path(conn, :index), Routes.home_path(conn, :folio)]
|> Enum.member?(Phoenix.Controller.current_path(conn))
end
def nav_link_opts(conn, opts) do
case Keyword.get(opts, :to) == Phoenix.Controller.current_path(conn) do
false -> opts