diff --git a/lib/mse25/directus.ex b/lib/mse25/directus.ex
index e261cb7..6e2fb25 100644
--- a/lib/mse25/directus.ex
+++ b/lib/mse25/directus.ex
@@ -1,4 +1,19 @@
defmodule Mse25.Directus do
+ @moduledoc """
+ Simple Directus client, utilizing Req to do CRUD
+ operations.
+
+ Currently, this client only read data, and supports
+ various ways of filtering data.
+
+ It is by no means generic, since fieldsets are not
+ agnostic. It may however be used as a base to create
+ a more generic client implementation in Elixir.
+
+ Directus documentation:
+ https://docs.directus.io/
+ """
+
@draft_filter "filter[status][_eq]=published"
def get_article(slug) do
@@ -15,7 +30,8 @@ defmodule Mse25.Directus do
"slug",
"title",
"date_updated",
- "pubDate"
+ "pubDate",
+ "contents"
],
","
)
@@ -57,9 +73,22 @@ defmodule Mse25.Directus do
|> query_params_string(options, :brutal_legends)
get("/albums?" <> params)
- |> Enum.map(fn m = %{"songs" => [%{"artist" => %{"name" => a}} | _], "purchased_at" => pa} ->
- m |> Map.put("artist", a) |> Map.put("purchase_year", String.slice(pa, 0..3))
- end)
+ |> Enum.map(
+ fn m = %{
+ "album" => album,
+ "year" => year,
+ "songs" => [%{"artist" => %{"name" => artist}} | _],
+ "purchased_at" => purchased_at
+ } ->
+ m
+ |> Map.put("artist", artist)
+ |> Map.put(
+ "purchase_year",
+ String.slice(purchased_at, 0..3)
+ )
+ |> Map.put("summary", "#{artist} - #{album} (#{to_string(year)})")
+ end
+ )
end
def get_event(slug) do
@@ -98,6 +127,7 @@ defmodule Mse25.Directus do
"category",
"started_at",
"ended_at",
+ "contents",
"bands.artists_id.name",
"mia.artists_id.name",
"location.*"
diff --git a/lib/mse25/timeline.ex b/lib/mse25/timeline.ex
index dae3675..d4985a9 100644
--- a/lib/mse25/timeline.ex
+++ b/lib/mse25/timeline.ex
@@ -1,23 +1,35 @@
defmodule Mse25.Timeline do
alias Mse25.Directus
- def archive() do
+ @almost_infinity 9999
+
+ def archive(limit \\ @almost_infinity) do
items =
Task.await_many([
Task.async(fn -> Directus.get_albums!() end),
- Task.async(fn -> Directus.get_articles!(limit: 9999) end),
- Task.async(fn -> Directus.get_links!(limit: 9999) end),
- Task.async(fn -> Directus.get_events!(limit: 9999) end)
+ Task.async(fn -> Directus.get_articles!(limit: limit) end),
+ Task.async(fn -> Directus.get_links!(limit: limit) end),
+ Task.async(fn -> Directus.get_events!(limit: limit) end)
])
+
+ archive =
+ items
+ |> List.flatten()
+ |> Enum.sort_by(&sort_key/1)
+ |> Enum.reverse()
+ |> Enum.take(limit)
+ |> Enum.map(&categorize/1)
+
+ {:ok, %{archive: archive}}
end
def annual(year) do
items =
Task.await_many([
- Task.async(fn -> Directus.get_albums!(limit: 9999, year: year) end),
- Task.async(fn -> Directus.get_articles!(limit: 9999, year: year) end),
- Task.async(fn -> Directus.get_links!(limit: 9999, year: year) end),
- Task.async(fn -> Directus.get_events!(limit: 9999, year: year) end)
+ Task.async(fn -> Directus.get_albums!(limit: @almost_infinity, year: year) end),
+ Task.async(fn -> Directus.get_articles!(limit: @almost_infinity, year: year) end),
+ Task.async(fn -> Directus.get_links!(limit: @almost_infinity, year: year) end),
+ Task.async(fn -> Directus.get_events!(limit: @almost_infinity, year: year) end)
])
counts =
@@ -40,9 +52,9 @@ defmodule Mse25.Timeline do
def search(query) do
items =
Task.await_many([
- Task.async(fn -> Directus.get_articles!(limit: 9999, query: query) end),
- Task.async(fn -> Directus.get_links!(limit: 9999, query: query) end),
- Task.async(fn -> Directus.get_events!(limit: 9999, query: query) end)
+ Task.async(fn -> Directus.get_articles!(limit: @almost_infinity, query: query) end),
+ Task.async(fn -> Directus.get_links!(limit: @almost_infinity, query: query) end),
+ Task.async(fn -> Directus.get_events!(limit: @almost_infinity, query: query) end)
])
results =
diff --git a/lib/mse25_web.ex b/lib/mse25_web.ex
index 107b6b8..46af4be 100644
--- a/lib/mse25_web.ex
+++ b/lib/mse25_web.ex
@@ -26,13 +26,6 @@ defmodule Mse25Web do
# Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller
- import Phoenix.LiveView.Router
- end
- end
-
- def channel do
- quote do
- use Phoenix.Channel
end
end
@@ -49,23 +42,6 @@ defmodule Mse25Web do
end
end
- def live_view do
- quote do
- use Phoenix.LiveView,
- layout: {Mse25Web.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
@@ -81,16 +57,10 @@ defmodule Mse25Web do
defp html_helpers do
quote do
- # HTML escaping functionality
import Phoenix.HTML
- # Core UI components and translation
import Mse25Web.CoreComponents
import Mse25Web.Gettext
- # Shortcut for generating JS commands
- alias Phoenix.LiveView.JS
-
- # Routes generation with the ~p sigil
unquote(verified_routes())
end
end
diff --git a/lib/mse25_web/components/core_components.ex b/lib/mse25_web/components/core_components.ex
index 31d4ae8..2c0ff73 100644
--- a/lib/mse25_web/components/core_components.ex
+++ b/lib/mse25_web/components/core_components.ex
@@ -1,676 +1,2 @@
defmodule Mse25Web.CoreComponents do
- @moduledoc """
- Provides core UI components.
-
- At first glance, this module may seem daunting, but its goal is to provide
- core building blocks for your application, such as modals, tables, and
- forms. The components consist mostly of markup and are 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 Mse25Web.Gettext
-
- @doc """
- Renders a modal.
-
- ## Examples
-
- <.modal id="confirm-modal">
- This is a 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.
-
-
- """
- 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"""
-
-
-
-
-
- <.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"
- >
-
-
- <.icon name="hero-x-mark-solid" class="h-5 w-5" />
-
-
-
- <%= render_slot(@inner_block) %>
-
-
-
-
-
-
- """
- end
-
- @doc """
- Renders flash notices.
-
- ## Examples
-
- <.flash kind={:info} flash={@flash} />
- <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
- """
- attr :id, :string, 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
- assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
-
- ~H"""
- hide("##{@id}")}
- role="alert"
- class={[
- "fixed top-2 right-2 mr-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}
- >
-
- <.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 %>
-
-
<%= msg %>
-
- <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
-
-
- """
- 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"
- attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
-
- def flash_group(assigns) do
- ~H"""
-
- <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
- <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
- <.flash
- id="client-error"
- kind={:error}
- title={gettext("We can't find the internet")}
- phx-disconnected={show(".phx-client-error #client-error")}
- phx-connected={hide("#client-error")}
- hidden
- >
- <%= gettext("Attempting to reconnect") %>
- <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
-
-
- <.flash
- id="server-error"
- kind={:error}
- title={gettext("Something went wrong!")}
- phx-disconnected={show(".phx-server-error #server-error")}
- phx-connected={hide("#server-error")}
- hidden
- >
- <%= gettext("Hang in there while we get back on track") %>
- <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
-
-
- """
- 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
-
-
- """
- attr :for, :any, required: true, doc: "the data structure 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 multipart),
- 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}>
-
- <%= render_slot(@inner_block, f) %>
-
- <%= render_slot(action, f) %>
-
-
-
- """
- end
-
- @doc """
- Renders a button.
-
- ## Examples
-
- <.button>Send!
- <.button phx-click="go" class="ml-2">Send!
- """
- 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"""
-
- <%= render_slot(@inner_block) %>
-
- """
- end
-
- @doc """
- Renders an input with label and error messages.
-
- A `Phoenix.HTML.FormField` may be passed as argument,
- which is used to retrieve the input name, id, and values.
- Otherwise all attributes may be passed explicitly.
-
- ## Types
-
- This function accepts all HTML input types, considering that:
-
- * You may also set `type="select"` to render a `` tag
-
- * `type="checkbox"` is used exclusively to render boolean values
-
- * For live file uploads, see `Phoenix.Component.live_file_input/1`
-
- See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
- for more information. Unsupported types, such as hidden and radio,
- are best written directly in your templates.
-
- ## 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 month number password
- range 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(accept autocomplete capture cols disabled form list max maxlength min minlength
- multiple pattern placeholder readonly required rows size step)
-
- def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
- errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
-
- assigns
- |> assign(field: nil, id: assigns.id || field.id)
- |> assign(:errors, Enum.map(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"} = assigns) do
- assigns =
- assign_new(assigns, :checked, fn ->
- Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
- end)
-
- ~H"""
-
-
-
-
- <%= @label %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- def input(%{type: "select"} = assigns) do
- ~H"""
-
- <.label for={@id}><%= @label %>
-
- <%= @prompt %>
- <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- def input(%{type: "textarea"} = assigns) do
- ~H"""
-
- <.label for={@id}><%= @label %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- # All other inputs text, datetime-local, url, password, etc. are handled here...
- def input(assigns) do
- ~H"""
-
- <.label for={@id}><%= @label %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- @doc """
- Renders a label.
- """
- attr :for, :string, default: nil
- slot :inner_block, required: true
-
- def label(assigns) do
- ~H"""
-
- <%= render_slot(@inner_block) %>
-
- """
- end
-
- @doc """
- Generates a generic error message.
- """
- slot :inner_block, required: true
-
- def error(assigns) do
- ~H"""
-
- <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
- <%= render_slot(@inner_block) %>
-
- """
- 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"""
-
- """
- end
-
- @doc ~S"""
- Renders a table with generic styling.
-
- ## Examples
-
- <.table id="users" rows={@users}>
- <:col :let={user} label="id"><%= user.id %>
- <:col :let={user} label="username"><%= user.username %>
-
- """
- 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"""
-
-
-
-
- <%= col[:label] %>
-
- <%= gettext("Actions") %>
-
-
-
-
-
-
-
-
-
- <%= render_slot(col, @row_item.(row)) %>
-
-
-
-
-
-
-
- <%= render_slot(action, @row_item.(row)) %>
-
-
-
-
-
-
-
- """
- end
-
- @doc """
- Renders a data list.
-
- ## Examples
-
- <.list>
- <:item title="Title"><%= @post.title %>
- <:item title="Views"><%= @post.views %>
-
- """
- slot :item, required: true do
- attr :title, :string, required: true
- end
-
- def list(assigns) do
- ~H"""
-
-
-
-
<%= item.title %>
- <%= render_slot(item) %>
-
-
-
- """
- end
-
- @doc """
- Renders a back navigation link.
-
- ## Examples
-
- <.back navigate={~p"/posts"}>Back to posts
- """
- attr :navigate, :any, required: true
- slot :inner_block, required: true
-
- def back(assigns) do
- ~H"""
-
- <.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) %>
-
-
- """
- end
-
- @doc """
- Renders a [Heroicon](https://heroicons.com).
-
- Heroicons come in three styles – outline, solid, and mini.
- By default, the outline style is used, but solid and 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 the `deps/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"""
-
- """
- end
-
- ## JS Commands
-
- def show(js \\ %JS{}, selector) do
- JS.show(js,
- to: selector,
- time: 300,
- 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",
- time: 300,
- 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(Mse25Web.Gettext, "errors", msg, msg, count, opts)
- else
- Gettext.dgettext(Mse25Web.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
diff --git a/lib/mse25_web/components/layouts/app.html.heex b/lib/mse25_web/components/layouts/app.html.heex
index 7ffbd85..c753bc6 100644
--- a/lib/mse25_web/components/layouts/app.html.heex
+++ b/lib/mse25_web/components/layouts/app.html.heex
@@ -1,17 +1,3 @@
<%= @inner_content %>
-
diff --git a/lib/mse25_web/controllers/feed_controller.ex b/lib/mse25_web/controllers/feed_controller.ex
index c3500df..e4c702c 100644
--- a/lib/mse25_web/controllers/feed_controller.ex
+++ b/lib/mse25_web/controllers/feed_controller.ex
@@ -2,16 +2,52 @@ defmodule Mse25Web.FeedController do
use Mse25Web, :controller
alias Mse25.Directus
alias Mse25.Timeline
+ plug :put_layout, false
- def atom_feed() do
- :tbw
+ def feed(conn, _params) do
+ {:ok, %{archive: items}} = Timeline.archive(20)
+
+ text(
+ conn |> put_resp_content_type("application/rss+xml"),
+ items
+ |> Mse25Web.FeedView.rss(conn.host)
+ )
end
- def upcoming_events_ics() do
- :tbw
+ def calendar(conn, _) do
+ text(
+ conn |> put_resp_content_type("text/calendar"),
+ Directus.get_events!(upcoming: true, limit: 9999)
+ |> Enum.map(fn %{
+ "title" => title,
+ "lead" => lead,
+ "started_at" => starts_at,
+ "ended_at" => ends_at,
+ "location" => %{
+ "name" => venue,
+ "address" => region,
+ "position" => %{
+ "coordinates" => [lat, lng]
+ }
+ }
+ } ->
+ %{
+ title: title,
+ lead: lead,
+ region: region,
+ venue: venue,
+ latitude: lat,
+ longitude: lng,
+ all_day?: true,
+ starts_at: String.replace(starts_at, "-", ""),
+ ends_at: String.replace(ends_at, "-", "")
+ }
+ end)
+ |> Mse25Web.FeedView.calendar()
+ )
end
- def albums_json(conn, _) do
+ def albums(conn, _) do
json(
conn,
Directus.get_albums!()
@@ -41,7 +77,7 @@ defmodule Mse25Web.FeedController do
)
end
- def events_json(conn, _) do
+ def events(conn, _) do
json(
conn,
Directus.get_events!(limit: 9999)
@@ -73,7 +109,31 @@ defmodule Mse25Web.FeedController do
)
end
- def event_map_js() do
- :tbw
+ def interactive_event_map(conn, _) do
+ text(
+ conn |> put_resp_content_type("text/javascript"),
+ Directus.get_events!(limit: 9999)
+ |> Enum.map(fn %{
+ "title" => title,
+ "started_at" => date,
+ "location" => %{
+ "name" => venue,
+ "address" => region,
+ "position" => %{
+ "coordinates" => [lat, lng]
+ }
+ }
+ } ->
+ %{
+ title: title,
+ date: String.slice(date, 0..9),
+ region: region,
+ venue: venue,
+ longitude: lng,
+ latitude: lat
+ }
+ end)
+ |> Mse25Web.FeedView.event_map()
+ )
end
end
diff --git a/lib/mse25_web/controllers/feed_view.ex b/lib/mse25_web/controllers/feed_view.ex
new file mode 100644
index 0000000..6066dc6
--- /dev/null
+++ b/lib/mse25_web/controllers/feed_view.ex
@@ -0,0 +1,252 @@
+defmodule Mse25Web.FeedView do
+ use Mse25Web, :html
+
+ def rss(items, _host) do
+ ~s"""
+
+
+
+ madr.se
+ The online home of Anders Englöf Ytterström, a metalhead and musician living and working in Borlänge, Sweden.
+ sv
+ https://madr.se/
+ yttan@fastmail.se (Anders Englöf Ytterström)
+ yttan@fastmail.se (Anders Englöf Ytterström)
+
+ #{Enum.map(items, &rss_item/1)}
+
+
+ """
+ end
+
+ def calendar(upcoming) do
+ ~s"""
+ BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//https://madr.se//kommande-evenemang
+ METHOD:PUBLISH
+ #{upcoming |> Enum.map(fn %{title: title, starts_at: starts_at, ends_at: ends_at, longitude: longitude, latitude: latitude, lead: lead, venue: venue, region: region} -> ~s"""
+ BEGIN:VEVENT
+ UID:#{title}.#{starts_at}@madr.se
+ DTSTAMP:#{starts_at}T000000
+ DTSTART;VALUE=DATE:#{starts_at}
+ DTEND;VALUE=DATE:#{ends_at}
+ SUMMARY:#{title}
+ DESCRIPTION:#{lead}
+ LOCATION:#{venue}\, #{region}
+ GEO:#{latitude};#{longitude}
+ END:VEVENT
+ """ end) |> Enum.join("")}END:VCALENDAR
+ """
+ end
+
+ def event_map(markers) do
+ ~s"""
+ (function(g, document) {
+ "use strict";
+
+ const mapData = [
+ #{markers |> Enum.map(fn %{date: date, latitude: latitude, longitude: longitude, title: title, region: region, venue: venue} -> ~s"""
+ {
+ location: [#{longitude}, #{latitude}],
+ title: "#{title}",
+ date: "#{date}",
+ region: "#{region}",
+ venue: "#{venue}"
+ }
+ """ end) |> Enum.join(",")}
+ ]
+
+ // insert Leaflet styles ( ) to and
diff --git a/lib/mse25_web/controllers/page_html/home.html.heex b/lib/mse25_web/controllers/page_html/home.html.heex
index 7c70d10..966a6b3 100644
--- a/lib/mse25_web/controllers/page_html/home.html.heex
+++ b/lib/mse25_web/controllers/page_html/home.html.heex
@@ -34,7 +34,9 @@
Evenemangstidslinje
+
Delningar
@@ -58,6 +60,9 @@
Anders, 39, Hårdrockare
+
Kontakt & Kolofon
diff --git a/lib/mse25_web/router.ex b/lib/mse25_web/router.ex
index d54c115..186f52f 100644
--- a/lib/mse25_web/router.ex
+++ b/lib/mse25_web/router.ex
@@ -4,16 +4,26 @@ defmodule Mse25Web.Router do
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
- plug :fetch_live_flash
plug :put_root_layout, html: {Mse25Web.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
+ pipeline :scripts do
+ plug :accepts, ["js"]
+ plug :put_secure_browser_headers
+ end
+
pipeline :api do
plug :accepts, ["json"]
end
+ scope "/", Mse25Web do
+ pipe_through :scripts
+
+ get "/event-map.js", FeedController, :interactive_event_map
+ end
+
scope "/", Mse25Web do
pipe_through :browser
@@ -23,11 +33,10 @@ defmodule Mse25Web.Router do
get "/delningar", PageController, :links
get "/sok", PageController, :search
- # get "/kommande-evenemang.ics", EventController, :calendar
- # get "/event-map.js", EventController, :interactive_map
- # get "/prenumerera.xml", TimelineController, :feed
- get "/albums.json", FeedController, :albums_json
- get "/events.json", FeedController, :events_json
+ get "/prenumerera.xml", FeedController, :feed
+ get "/albums.json", FeedController, :albums
+ get "/events.json", FeedController, :events
+ get "/kommande-evenemang.ics", FeedController, :calendar
get "/*path", ItemController, :index
end