phx update
This commit is contained in:
@ -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
28
lib/homepage/release.ex
Normal 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
|
@ -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
|
||||
|
||||
|
640
lib/homepage_web/components/core_components.ex
Normal file
640
lib/homepage_web/components/core_components.ex
Normal 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
|
63
lib/homepage_web/components/helper_components.ex
Normal file
63
lib/homepage_web/components/helper_components.ex
Normal 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
|
6
lib/homepage_web/components/layouts.ex
Normal file
6
lib/homepage_web/components/layouts.ex
Normal file
@ -0,0 +1,6 @@
|
||||
defmodule HomepageWeb.Layouts do
|
||||
use HomepageWeb, :html
|
||||
import HomepageWeb.HelperComponents
|
||||
|
||||
embed_templates "layouts/*"
|
||||
end
|
32
lib/homepage_web/components/layouts/app.html.heex
Normal file
32
lib/homepage_web/components/layouts/app.html.heex
Normal 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>
|
24
lib/homepage_web/components/layouts/root.html.heex
Normal file
24
lib/homepage_web/components/layouts/root.html.heex
Normal 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>
|
19
lib/homepage_web/controllers/error_html.ex
Normal file
19
lib/homepage_web/controllers/error_html.ex
Normal 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
|
15
lib/homepage_web/controllers/error_json.ex
Normal file
15
lib/homepage_web/controllers/error_json.ex
Normal 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
|
@ -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
|
||||
|
34
lib/homepage_web/controllers/page_html.ex
Normal file
34
lib/homepage_web/controllers/page_html.ex
Normal 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
|
42
lib/homepage_web/controllers/page_html/home.html.heex
Normal file
42
lib/homepage_web/controllers/page_html/home.html.heex
Normal 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>
|
17
lib/homepage_web/controllers/page_html/phoenix.html.heex
Normal file
17
lib/homepage_web/controllers/page_html/phoenix.html.heex
Normal 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>
|
@ -1,7 +0,0 @@
|
||||
defmodule HomepageWeb.ResumeController do
|
||||
use HomepageWeb, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html")
|
||||
end
|
||||
end
|
@ -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.
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,3 +0,0 @@
|
||||
<article class="prose mx-auto">
|
||||
<div class="font-mono">ch</div>
|
||||
</article>
|
@ -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>
|
@ -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
|
@ -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
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
defmodule HomepageWeb.PageView do
|
||||
use HomepageWeb, :view
|
||||
end
|
@ -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
|
Reference in New Issue
Block a user