phx update

This commit is contained in:
2023-11-07 08:52:09 +01:00
parent 8ad54eda1c
commit b2439d7251
974 changed files with 4538 additions and 1120 deletions

View File

@ -8,12 +8,14 @@ defmodule Homepage.Application do
@impl true
def start(_type, _args) do
children = [
# Start the Ecto repository
Homepage.Repo,
# Start the Telemetry supervisor
HomepageWeb.Telemetry,
# Start the Ecto repository
Homepage.Repo,
# Start the PubSub system
{Phoenix.PubSub, name: Homepage.PubSub},
# Start Finch
{Finch, name: Homepage.Finch},
# Start the Endpoint (http/https)
HomepageWeb.Endpoint
# Start a worker by calling: Homepage.Worker.start_link(arg)

28
lib/homepage/release.ex Normal file
View File

@ -0,0 +1,28 @@
defmodule Homepage.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :homepage
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end

View File

@ -1,76 +1,30 @@
defmodule HomepageWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
as controllers, components, channels, and so on.
This can be used in your application as:
use HomepageWeb, :controller
use HomepageWeb, :view
use HomepageWeb, :html
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
The definitions below will be executed for every controller,
component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
below. Instead, define additional modules and import
those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: HomepageWeb
import Plug.Conn
import HomepageWeb.Gettext
alias HomepageWeb.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View,
root: "lib/homepage_web/templates",
namespace: HomepageWeb
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
def live_view do
quote do
use Phoenix.LiveView,
layout: {HomepageWeb.LayoutView, "live.html"}
unquote(view_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
unquote(view_helpers())
end
end
def component do
quote do
use Phoenix.Component
unquote(view_helpers())
end
end
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def static_digested, do: ~w(android-chrome apple-touch-icon favicon site.webmanifest)
def router do
quote do
use Phoenix.Router
use Phoenix.Router, helpers: false
# Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
@ -80,24 +34,74 @@ defmodule HomepageWeb do
def channel do
quote do
use Phoenix.Channel
import HomepageWeb.Gettext
end
end
defp view_helpers do
def controller do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: HomepageWeb.Layouts]
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import HomepageWeb.ErrorHelpers
import Plug.Conn
import HomepageWeb.Gettext
alias HomepageWeb.Router.Helpers, as: Routes
unquote(verified_routes())
end
end
def live_view do
quote do
use Phoenix.LiveView,
layout: {HomepageWeb.Layouts, :app}
unquote(html_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
unquote(html_helpers())
end
end
def html do
quote do
use Phoenix.Component
# Import convenience functions from controllers
import Phoenix.Controller,
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
# Include general helpers for rendering HTML
unquote(html_helpers())
end
end
defp html_helpers do
quote do
# HTML escaping functionality
import Phoenix.HTML
# Core UI components and translation
import HomepageWeb.CoreComponents
import HomepageWeb.Gettext
# Shortcut for generating JS commands
alias Phoenix.LiveView.JS
# Routes generation with the ~p sigil
unquote(verified_routes())
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: HomepageWeb.Endpoint,
router: HomepageWeb.Router,
statics: HomepageWeb.static_paths()
end
end

View File

@ -0,0 +1,640 @@
defmodule HomepageWeb.CoreComponents do
@moduledoc """
Provides core UI components.
At the first glance, this module may seem daunting, but its goal is
to provide some core building blocks in your application, such modals,
tables, and forms. The components are mostly markup and well documented
with doc strings and declarative assigns. You may customize and style
them in any way you want, based on your application growth and needs.
The default components use Tailwind CSS, a utility-first CSS framework.
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
how to customize them or feel free to swap in another framework altogether.
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
"""
use Phoenix.Component
alias Phoenix.LiveView.JS
import HomepageWeb.Gettext
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
This is a modal.
</.modal>
JS commands may be passed to the `:on_cancel` to configure
the closing/cancel event, for example:
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
This is another modal.
</.modal>
"""
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :on_cancel, JS, default: %JS{}
slot :inner_block, required: true
def modal(assigns) do
~H"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
>
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
>
<div class="absolute top-6 right-5">
<button
phx-click={JS.exec("data-cancel", to: "##{@id}")}
type="button"
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
</button>
</div>
<div id={"#{@id}-content"}>
<%= render_slot(@inner_block) %>
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
@doc """
Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
"""
attr :id, :string, default: "flash", doc: "the optional id of flash container"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :title, :string, default: nil
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do
~H"""
<div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={[
"fixed top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
]}
{@rest}
>
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
<%= @title %>
</p>
<p class="mt-2 text-sm leading-5"><%= msg %></p>
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
</button>
</div>
"""
end
@doc """
Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} />
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
def flash_group(assigns) do
~H"""
<.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} />
<.flash
id="disconnected"
kind={:error}
title="We can't find the internet"
phx-disconnected={show("#disconnected")}
phx-connected={hide("#disconnected")}
hidden
>
Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
</.flash>
"""
end
@doc """
Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label="Email"/>
<.input field={@form[:username]} label="Username" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
attr :for, :any, required: true, doc: "the datastructure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global,
include: ~w(autocomplete name rel action enctype method novalidate target),
doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white">
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
@doc """
Renders an input with label and error messages.
A `%Phoenix.HTML.Form{}` and field name may be passed to the input
to build input names and error messages, or all the attributes and
errors may be passed explicitly.
## Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr :id, :any, default: nil
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: []
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :rest, :global,
include: ~w(autocomplete cols disabled form list max maxlength min minlength
pattern placeholder readonly required rows size step)
slot :inner_block
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(%{type: "checkbox", value: value} = assigns) do
assigns =
assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
~H"""
<div phx-feedback-for={@name}>
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
<input type="hidden" name={@name} value="false" />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
<%= @label %>
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<select
id={@id}
name={@name}
class="mt-1 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id}
name={@name}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
"min-h-[6rem] border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
"border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@doc """
Renders a label.
"""
attr :for, :string, default: nil
slot :inner_block, required: true
def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
<%= render_slot(@inner_block) %>
</label>
"""
end
@doc """
Generates a generic error message.
"""
slot :inner_block, required: true
def error(assigns) do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600 phx-no-feedback:hidden">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %>
</p>
"""
end
@doc """
Renders a header with title.
"""
attr :class, :string, default: nil
slot :inner_block, required: true
slot :subtitle
slot :actions
def header(assigns) do
~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
<%= render_slot(@inner_block) %>
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
<%= render_slot(@subtitle) %>
</p>
</div>
<div class="flex-none"><%= render_slot(@actions) %></div>
</header>
"""
end
@doc ~S"""
Renders a table with generic styling.
## Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
</.table>
"""
attr :id, :string, required: true
attr :rows, :list, required: true
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
slot :col, required: true do
attr :label, :string
end
slot :action, doc: "the slot for showing user actions in the last table column"
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="p-0 pr-6 pb-4 font-normal"><%= col[:label] %></th>
<th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
</td>
<td :if={@action != []} class="relative w-14 p-0">
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
<span
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<%= render_slot(action, @row_item.(row)) %>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
"""
end
@doc """
Renders a data list.
## Examples
<.list>
<:item title="Title"><%= @post.title %></:item>
<:item title="Views"><%= @post.views %></:item>
</.list>
"""
slot :item, required: true do
attr :title, :string, required: true
end
def list(assigns) do
~H"""
<div class="mt-14">
<dl class="-my-4 divide-y divide-zinc-100">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
</div>
</dl>
</div>
"""
end
@doc """
Renders a back navigation link.
## Examples
<.back navigate={~p"/posts"}>Back to posts</.back>
"""
attr :navigate, :any, required: true
slot :inner_block, required: true
def back(assigns) do
~H"""
<div class="mt-16">
<.link
navigate={@navigate}
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
<%= render_slot(@inner_block) %>
</.link>
</div>
"""
end
@doc """
Renders a [Hero Icon](https://heroicons.com).
Hero icons come in three styles outline, solid, and mini.
By default, the outline style is used, but solid an mini may
be applied by using the `-solid` and `-mini` suffix.
You can customize the size and colors of the icons by setting
width, height, and background color classes.
Icons are extracted from your `assets/vendor/heroicons` directory and bundled
within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-x-mark-solid" />
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
def icon(%{name: "hero-" <> _} = assigns) do
~H"""
<span class={[@name, @class]} />
"""
end
## JS Commands
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
transition:
{"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
end
def hide(js \\ %JS{}, selector) do
JS.hide(js,
to: selector,
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(HomepageWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(HomepageWeb.Gettext, "errors", msg, opts)
end
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
end

View File

@ -0,0 +1,63 @@
defmodule HomepageWeb.HelperComponents do
use HomepageWeb, :html
slot :label, required: true
slot :entry, default: []
def dropdown(assigns) do
~H"""
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost m-1">
<%= render_slot(@label) %>
</label>
<ul tabindex="0"
class="dropdown-content menu p-2 shadow rounded-box">
<li :for={ent <- @entry}>
<%= render_slot(ent) %>
</li>
</ul>
</div>
"""
end
slot :inner_block, required: true
def markdown(assigns) do
~H"""
<%= render_slot(@inner_block)
|> slot_markdown_as_html()
|> raw()
%>
"""
end
defp slot_markdown_as_html(%{dynamic: dynamic_slot, static: ["", ""]}) do
[slot] = dynamic_slot.(false)
slot |> slot_markdown_as_html()
end
defp slot_markdown_as_html(rendered_slot) do
%{static: [markdown]} = rendered_slot
trim_leading_space(markdown)
|> String.replace(~S("\""), ~S("""), global: true)
|> Earmark.as_html!(compact_output: true)
end
defp trim_leading_space(markdown) do
lines =
markdown
|> String.split("\n")
|> Enum.drop_while(fn str -> String.trim(str) == "" end)
case lines do
[first | _] ->
[space] = Regex.run(~r/^\s*/, first)
lines
|> Enum.map(fn line -> String.replace_prefix(line, space, "") end)
|> Enum.join("\n")
_ ->
""
end
end
end

View File

@ -0,0 +1,6 @@
defmodule HomepageWeb.Layouts do
use HomepageWeb, :html
import HomepageWeb.HelperComponents
embed_templates "layouts/*"
end

View File

@ -0,0 +1,32 @@
<header class="px-4 sm:px-6 lg:px-8">
<.dropdown>
<:label>@rdiedrich</:label>
<:entry>
<.link
title="send an e-mail"
href="mailto:hallo@rdiedri.ch">
<.icon name="hero-envelope" />
hallo@rdiedri.ch
</.link>
</:entry>
<:entry>
<.link
title="chat with me on matrix"
href="https://matrix.to/#/@rdiedrich:matrix.org"
target="_blank">
<.icon name="hero-chat-bubble-bottom-center-text" />
@rdiedrich:matrix.org
</.link>
</:entry>
</.dropdown>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-16">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
<footer>
</footer>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en" style="scrollbar-gutter: stable;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix="">
<%= assigns[:page_title] || "Homepage" %>
</.live_title>
<link rel="apple-touch-icon" sizes="180x180"
href={static_path(@conn, ~p"/apple-touch-icon.png")}>
<link rel="icon" type="image/png" sizes="32x32"
href={static_path(@conn, ~p"/favicon-32x32.png")}>
<link phx-track-static rel="icon" type="image/png" sizes="16x16"
href={static_path(@conn, ~p"/favicon-16x16.png")}>
<link rel="manifest" href="/site.webmanifest">
<link phx-track-static rel="stylesheet" href={static_url(@conn, ~p"/assets/app.css")} />
<script defer phx-track-static type="text/javascript" src={static_url(@conn, ~p"/assets/app.js")}>
</script>
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</html>

View File

@ -0,0 +1,19 @@
defmodule HomepageWeb.ErrorHTML do
use HomepageWeb, :html
# If you want to customize your error pages,
# uncomment the embed_templates/1 call below
# and add pages to the error directory:
#
# * lib/homepage_web/controllers/error_html/404.html.heex
# * lib/homepage_web/controllers/error_html/500.html.heex
#
# embed_templates "error_html/*"
# The default is to render a plain text page based on
# the template name. For example, "404.html" becomes
# "Not Found".
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View File

@ -0,0 +1,15 @@
defmodule HomepageWeb.ErrorJSON do
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@ -1,7 +1,10 @@
defmodule HomepageWeb.PageController do
use HomepageWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
def home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
#render(conn, :home, layout: false)
render(conn, :home)
end
end

View File

@ -0,0 +1,34 @@
defmodule HomepageWeb.PageHTML do
use HomepageWeb, :html
import HomepageWeb.HelperComponents
embed_templates "page_html/*"
attr :url, :string, required: true
attr :forge_url, :string, default: nil
slot :inner_block, required: true
slot :title, required: true
def project(assigns) do
~H"""
<div class="project">
<.link target="_blank" class="" href={@url}>
<h3><%= render_slot(@title) %></h3>
</.link>
<div class="desc">
<.markdown><%= render_slot(@inner_block) %></.markdown>
</div>
<div class="links mt-1 text-indigo flex gap-4 justify-start">
<.link target="_blank" href={@url} class="icon-link">
<.icon name="hero-arrow-top-right-on-square" />
Visit website
</.link>
<.link target="_blank" href={@forge_url} :if={@forge_url} class="icon-link">
<.icon name="hero-code-bracket-square" />
See the code
</.link>
</div>
</div>
"""
end
end

View File

@ -0,0 +1,42 @@
<section class="about">
<h2>about</h2>
<.markdown>
Hi, my name is **Rüdiger Diedrich** and this is my homepage.
For the last couple of years I am a big proponent of the Elixir and
Erlang/OTP ecosystem: be it projects like Phoenix framework which
bends the rules of traditional client-server-based web development
or Livebook - built on top of Phoenix - which for me is simply the
next generation of interactive notebooks and completely changed
the way I go about prototyping and data analysis.
Check out some of the projects I've been working on.
</.markdown>
</section>
<section class="projects">
<h2>projects</h2>
<.project
url="https://chicken.rdiedri.ch"
forge_url="https://forge.rdiedri.ch/rdiedrich/exponential-chicken-egg">
<:title>Exponential Chicken Egg</:title>
The chicken is very busy.
100% implemented in Phoenix Liveview. Press spacebar (or tap) for fun.
</.project>
<.project
url="https://app.rdiedri.ch"
forge_url="https://forge.rdiedri.ch/rdiedrich/physics">
<:title>Physics</:title>
Random falling blocks under the yoke of gravity.
Typescript using the pixi engine. Click or tap a block to give it a boost.
</.project>
<.project
url="https://colorer.vercel.app"
forge_url="https://forge.rdiedri.ch/rdiedrich/colorer">
<:title>Colorer</:title>
Play around with HSLA color.
Reactive app show-casing SolidJS, deployed on vercel.
</.project>
</section>
<p>a</p>

View File

@ -0,0 +1,17 @@
<section class="phoenix">
<h2>Phoenix framework</h2>
<.markdown>
Phoenix is enabling developers to built concurrent distributed
systems without the technological and mental overhead this usually
requires, while also offering modern frontend engineering principles
in component-based design.
On the other side of the equation, end-users can expect highly
interactive applications with built-in live collaboration and
featuring user interfaces that feel snappy to use and are also
pretty to look at.
And all of this at a fraction of the needed ressources on the
engineering side to built as well as on the infrastructure side
to run these systems.
</.markdown>
</section>

View File

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

View File

@ -7,7 +7,8 @@ defmodule HomepageWeb.Endpoint do
@session_options [
store: :cookie,
key: "_homepage_key",
signing_salt: "HBldD12f"
signing_salt: "gxQ6oTQt",
same_site: "Lax"
]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
@ -19,8 +20,10 @@ defmodule HomepageWeb.Endpoint do
plug Plug.Static,
at: "/",
from: :homepage,
gzip: false,
only: ~w(assets fonts images favicon.ico robots.txt)
headers: [{"access-control-allow-origin", "*"}],
gzip: true,
only_matching: HomepageWeb.static_digested(),
only: HomepageWeb.static_paths()
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.

View File

@ -1,37 +0,0 @@
defmodule HomepageWeb.Icons do
import Phoenix.LiveView.Helpers
@priv_dir Path.join(:code.priv_dir(:homepage), "icons")
@repo_url "https://github.com/CoreyGinnivan/system-uicons.git"
System.cmd("rm", ["-rf", Path.join(@priv_dir, "system-uicons")])
System.cmd("git", ["clone", "--depth=1", @repo_url, Path.join(@priv_dir, "system-uicons")])
source_data = File.read!(Path.join(@priv_dir, "system-uicons/src/js/data.js"))
<<"var sourceData = "::utf8 , rest::binary>> = source_data
# remove trailing semicolon
sslice = String.slice(rest, 0..-3//1)
# quote object keys
quote_keys = Regex.replace(~r/([\w_]+):/, sslice, "\"\\1\":")
# remove trailing commas
rm_trailing_commas = Regex.replace(~r/,\s+(}|])/, quote_keys, "\\1")
icon_data = Jason.decode!(rm_trailing_commas)
icon_map = Enum.map(icon_data, fn %{"icon_path" => path} = icon ->
svg = File.read!(Path.join(@priv_dir, "system-uicons/src/images/icons/#{path}.svg"))
Map.put_new(icon, "icon_svg", svg)
|> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
end)
for %{icon_path: path, icon_svg: svg} <- icon_map do
def unquote(String.to_atom(path))(assigns) do
svg = unquote(svg)
~H"""
<i class={"icon"}>
<%= Phoenix.HTML.raw svg %>
</i>
"""
end
end
end

View File

@ -5,7 +5,7 @@ defmodule HomepageWeb.Router do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {HomepageWeb.LayoutView, :root}
plug :put_root_layout, {HomepageWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
@ -17,8 +17,7 @@ defmodule HomepageWeb.Router do
scope "/", HomepageWeb do
pipe_through :browser
get "/", PageController, :index
get "/resume", ResumeController, :index
get "/", PageController, :home
end
# Other scopes may use custom stacks.
@ -26,31 +25,19 @@ defmodule HomepageWeb.Router do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:homepage, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: HomepageWeb.Telemetry
end
end
# Enables the Swoosh mailbox preview in development.
#
# Note that preview only shows emails that were sent by the same
# node running the Phoenix server.
if Mix.env() == :dev do
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: HomepageWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end

View File

@ -22,13 +22,34 @@ defmodule HomepageWeb.Telemetry do
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.start.system_time",
unit: {:native, :millisecond}
),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.start.system_time",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.exception.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.socket_connected.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_join.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_handled_in.duration",
tags: [:event],
unit: {:native, :millisecond}
),
# Database Metrics
summary("homepage.repo.query.total_time",

View File

@ -1,5 +0,0 @@
<main class="">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %>
</main>

View File

@ -1,11 +0,0 @@
<main class="container">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
<%= @inner_content %>
</main>

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "rdiedri.ch", suffix: "" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
</head>
<body id="root">
<%= @inner_content %>
</body>
</html>

View File

@ -1,3 +0,0 @@
<article class="prose mx-auto">
<div class="font-mono">ch</div>
</article>

View File

@ -1,103 +0,0 @@
<div class="flex">
<Icons.phone_portrait />
<Icons.paper_plane />
<Icons.location />
</div>
<div class="grid grid-cols-2">
<section>
<.article heading="Berufserfahrung">
<.para
heading="Spectrum Wirtschaftswerbung"
subheading="April 2019 heute">
Produktentwicklung Augmented Reality, Online-Magazin;
Projektleitung, technische Betreuung, Web-Entwicklung, Web-Design
</.para>
<.para
heading="Selbstständige Tätigkeit, Magdeburg"
subheading="Oktober 2010 April 2019">
u.a. Abtshof: Webdesign, Logopädische Praxis - Katharina Neils: Webdesign
</.para>
<.para
heading="Galerie Atelier Bischof, Karlsruhe"
subheading="2010">
Erfahrungen im Bereich Kommunikationsdesign, Erweiterung von wordpress-Systemen, Entwicklung einer E-Commerce-Lösung basierend auf Satchmo, Frontend-Design und Umsetzung</.para>
<.para
heading="scriptmesh, Karlsruhe"
subheading="November 2009 Mai 2010">
Software-Entwickler in einem Startup, Planung, Entwurf und Entwicklung einer Dokumenten-Archivierung- und Ausstausch-Plattform
</.para>
<.para
heading="brandmaker GmbH, Karlsruhe"
subheading="Mai 2008 Oktober 2009">
PHP-Entwickler, Frontend-Entwickler, Projektleitung, Kundenbetreuung, Wartung und Weiterentwicklung des hauseigenen CMS „OSIRIS“, Template-Programmierung
</.para>
<.para
heading="Stadtjugendausschuss Karlsruhe"
subheading="Oktober 2007 Mai 2008">
IT-Assistent und Assistent der Medienpädagogik, Einrichtung und Wartung der Desktop-Computer und der mobilen Computeranlage, Betreuung und Schulung von Kindern und Jugendlichen in einem Internet-Café sowie in Computer-Kursen
</.para>
<.para
heading="Chrystall-Net, Magdeburg"
subheading="September 2003 - Juli 2005">
Entwicklung einer Spieleseite, Datenbank-Programmierung, Benutzermanagement
</.para>
</.article>
<.article heading="Projekte"></.article>
</section>
<section>
<.article
heading="Kompetenzen">
</.article>
<.article
heading="Bildung">
<.para
heading="Karlsruher Institut für Technologie"
subheading="Oktober 2005 September 2007">
Informatikstudium, Diplomstudiengang
</.para>
<.para
heading="Otto-v-Guericke-Universität Magdeburg"
subheading="April 2002 September 2003">
Philosophie- / Soziologiestudium, Magisterstudiengang
</.para>
<.para
heading="Albert-Einstein-Gymnasium Magdeburg"
subheading="September 1993 Juli 2000">
Abschluss: Abitur
</.para>
</.article>
<.article
heading="Sprachkenntnisse">
</.article>
</section>
</div>
<article class="prose absolute bottom-0 right-0 flex">
<div class="color-box bg-blueViolet"></div>
<div class="color-box bg-paradisePink"></div>
<div class="color-box bg-sunglow"></div>
<div class="color-box bg-honeydew"></div>
<div class="color-box bg-pineTree"></div>
</article>

View File

@ -1,47 +0,0 @@
defmodule HomepageWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: "invalid-feedback",
phx_feedback_for: input_name(form, field)
)
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# 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(HomepageWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(HomepageWeb.Gettext, "errors", msg, opts)
end
end
end

View File

@ -1,16 +0,0 @@
defmodule HomepageWeb.ErrorView do
use HomepageWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
# def render("500.html", _assigns) do
# "Internal Server Error"
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View File

@ -1,7 +0,0 @@
defmodule HomepageWeb.LayoutView do
use HomepageWeb, :view
# Phoenix LiveDashboard is available only in development by default,
# so we instruct Elixir to not warn if the dashboard route is missing.
@compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
end

View File

@ -1,3 +0,0 @@
defmodule HomepageWeb.PageView do
use HomepageWeb, :view
end

View File

@ -1,29 +0,0 @@
defmodule HomepageWeb.ResumeView do
use HomepageWeb, :view
alias HomepageWeb.Icons
def para(assigns) do
~H"""
<div>
<h3 class="font-semibold text-lg mb-0"><%= @heading %></h3>
<%= if assigns[:subheading] do %>
<h4 class="italic mb-2"><%= @subheading %></h4>
<% end %>
<p class="mb-4">
<%= render_slot(@inner_block) %>
</p>
</div>
"""
end
def article(assigns) do
~H"""
<article>
<h2 class="font-['Inter'] font-semibold text-xl">
<%= @heading %></h2>
<%= render_slot(@inner_block) %>
</article>
"""
end
end