From d1fad5f55ef1b0d6960045669476f69df1b8fffd Mon Sep 17 00:00:00 2001 From: Jerod Santo Date: Thu, 25 Jul 2024 10:30:19 -0500 Subject: [PATCH] First steps toward feed management --- assets/app/app.js | 41 +++++++ assets/app/components/form.scss | 44 +++++-- lib/changelog/policies/feed.ex | 15 ++- lib/changelog/schema/person.ex | 3 + .../controllers/home/feed_controller.ex | 111 ++++++++++++++++++ .../controllers/{ => home}/home_controller.ex | 0 lib/changelog_web/router.ex | 2 + .../templates/home/_nav.html.eex | 20 ---- .../templates/home/_nav.html.heex | 25 ++++ .../templates/home/feed/_form.html.heex | 98 ++++++++++++++++ .../templates/home/feed/edit.html.heex | 6 + .../templates/home/feed/index.html.heex | 42 +++++++ .../templates/home/feed/new.html.heex | 6 + .../templates/home/show.html.heex | 3 +- lib/changelog_web/views/home/feed_view.ex | 6 + 15 files changed, 390 insertions(+), 32 deletions(-) create mode 100644 lib/changelog_web/controllers/home/feed_controller.ex rename lib/changelog_web/controllers/{ => home}/home_controller.ex (100%) delete mode 100644 lib/changelog_web/templates/home/_nav.html.eex create mode 100644 lib/changelog_web/templates/home/_nav.html.heex create mode 100644 lib/changelog_web/templates/home/feed/_form.html.heex create mode 100644 lib/changelog_web/templates/home/feed/edit.html.heex create mode 100644 lib/changelog_web/templates/home/feed/index.html.heex create mode 100644 lib/changelog_web/templates/home/feed/new.html.heex create mode 100644 lib/changelog_web/views/home/feed_view.ex diff --git a/assets/app/app.js b/assets/app/app.js index b2aa2b7d0d..1d9bdd1e4e 100644 --- a/assets/app/app.js +++ b/assets/app/app.js @@ -131,6 +131,47 @@ u(document).handle("click", ".js-subscribe-all", function (event) { }); }); +// Manage custom feed podcasts +u(document).on("click", ".js-feed-podcast_ids button", function (event) { + event.preventDefault(); + let podcast = u(event.target).closest("button"); + let id = podcast.data("id"); + + if (podcast.hasClass("disabled")) { + podcast.removeClass("disabled"); + podcast.append( + `` + ); + } else { + podcast.addClass("disabled"); + podcast.find("input").remove(); + } +}); + +// Manage custom feed covers +u(document).on("change", ".js-feed-cover_select", function (event) { + let coverUrl = event.target.value; + + u(".js-feed-cover_field").find("img").attr("src", coverUrl); + u(".js-feed-cover_field").find("input[type=hidden]").first().value = coverUrl; +}); + +u(document).on( + "change", + ".js-feed-cover_field input[type=file]", + function (event) { + let file = event.target.files[0]; + + if (file) { + let reader = new FileReader(); + reader.onload = function (e) { + u(".js-feed-cover_field").find("img").attr("src", e.target.result); + }; + reader.readAsDataURL(file); + } + } +); + u(document).handle("click", ".js-toggle_element", function (event) { const href = u(event.target).attr("href"); u(href).toggleClass("is-hidden"); diff --git a/assets/app/components/form.scss b/assets/app/components/form.scss index b984f6154d..9fe705e259 100644 --- a/assets/app/components/form.scss +++ b/assets/app/components/form.scss @@ -53,6 +53,7 @@ } textarea, + select, input { border: none; border-bottom: $border; @@ -62,6 +63,7 @@ line-height: 1.5em; padding: 0.75em 0; width: 100%; + min-height: 55px; &:disabled { background: none; @@ -89,7 +91,9 @@ font-style: italic; margin: 5px 0; } - &-error { color: $red; } + &-error { + color: $red; + } } &-submit { @@ -110,7 +114,9 @@ max-width: 340px; text-align: left; - @include breakpoint(mobile) { margin-bottom: 0; } + @include breakpoint(mobile) { + margin-bottom: 0; + } a { color: $blue-grey; @@ -132,7 +138,9 @@ padding: 0.5em 30px 0.4em; transition: background 0.1s $base-easing; - &:hover { background: darken($green, 10%); } + &:hover { + background: darken($green, 10%); + } } input[disabled] { @@ -163,8 +171,10 @@ user-select: none; } - input[type=checkbox] { display: none; } - input[type=checkbox] + .form-checklist-item-box { + input[type="checkbox"] { + display: none; + } + input[type="checkbox"] + .form-checklist-item-box { flex: 0 0 21px; border: 1px solid $black; display: block; @@ -174,10 +184,30 @@ position: relative; top: -1px; } - input[type=checkbox]:checked + .form-checklist-item-box { - background: url('../images/icons/form-checkmark.svg') center no-repeat; + input[type="checkbox"]:checked + .form-checklist-item-box { + background: url("../images/icons/form-checkmark.svg") center no-repeat; } } } } + + &-grid { + display: flex; + flex-wrap: wrap; + + &-card { + float: none; + width: calc(25% - 1.5em); + margin: 0.75em; + + .disabled { + opacity: 0.45; + } + + img { + display: block; + width: 100%; + } + } + } } diff --git a/lib/changelog/policies/feed.ex b/lib/changelog/policies/feed.ex index 3380f7e9a0..8fab76dde0 100644 --- a/lib/changelog/policies/feed.ex +++ b/lib/changelog/policies/feed.ex @@ -1,11 +1,18 @@ defmodule Changelog.Policies.Feed do use Changelog.Policies.Default - def index(actor), do: is_active_member(actor) - def create(actor), do: is_active_member(actor) + def index(actor), do: is_admin_or_active_member(actor) - def update(actor, feed), do: is_owner(actor, feed) - def delete(actor, feed), do: is_owner(actor, feed) + def new(actor), do: is_admin_or_active_member(actor) + def create(actor), do: new(actor) + + def edit(actor, feed), do: is_owner(actor, feed) + def update(actor, feed), do: edit(actor, feed) + def delete(actor, feed), do: edit(actor, feed) + + defp is_admin_or_active_member(actor) do + is_admin(actor) || is_active_member(actor) + end defp is_active_member(nil), do: false defp is_active_member(actor), do: Map.get(actor, :active_membership, false) diff --git a/lib/changelog/schema/person.ex b/lib/changelog/schema/person.ex index 9792373c7a..b43fec2eac 100644 --- a/lib/changelog/schema/person.ex +++ b/lib/changelog/schema/person.ex @@ -8,6 +8,7 @@ defmodule Changelog.Person do EpisodeHost, EpisodeRequest, Faker, + Feed, Files, Membership, NewsItem, @@ -96,6 +97,8 @@ defmodule Changelog.Person do has_many :subscriptions, Subscription, where: [unsubscribed_at: nil], on_delete: :delete_all has_many :episode_requests, EpisodeRequest, foreign_key: :submitter_id, on_delete: :delete_all + has_many :feeds, Feed, foreign_key: :owner_id + timestamps() end diff --git a/lib/changelog_web/controllers/home/feed_controller.ex b/lib/changelog_web/controllers/home/feed_controller.ex new file mode 100644 index 0000000000..a8c421a96f --- /dev/null +++ b/lib/changelog_web/controllers/home/feed_controller.ex @@ -0,0 +1,111 @@ +defmodule ChangelogWeb.Home.FeedController do + use ChangelogWeb, :controller + + alias Changelog.{Feed, Podcast} + alias Changelog.ObanWorkers.FeedUpdater + + plug :assign_podcasts when action in [:index, :new, :create, :edit, :update] + plug :assign_feed when action in [:edit, :update, :delete, :refresh] + plug Authorize, [Policies.Feed, :feed] + plug :preload_current_user_extras + + def index(conn = %{assigns: %{current_user: me}}, _params) do + feeds = me |> assoc(:feeds) |> Repo.all() + + conn + |> assign(:feeds, feeds) + |> render() + end + + def new(conn, _params) do + changeset = Feed.insert_changeset(%Feed{}) + + conn + |> assign(:changeset, changeset) + |> render(:new) + end + + def create(conn = %{assigns: %{current_user: user}}, %{"feed" => feed_params}) do + changeset = Feed.insert_changeset(%Feed{owner_id: user.id}, feed_params) + + case Repo.insert(changeset) do + {:ok, feed} -> + Repo.update(Feed.file_changeset(feed, feed_params)) + + conn + |> put_flash(:success, "Your new feed has been created! 👏") + |> redirect(to: ~p"/~/feeds") + + {:error, changeset} -> + require IEx + IEx.pry() + + conn + |> put_flash(:error, "There was a problem saving your feed. 😭") + |> assign(:changeset, changeset) + |> render(:new) + end + end + + def edit(conn = %{assigns: %{feed: feed}}, _params) do + changeset = Feed.update_changeset(feed) + render(conn, :edit, feed: feed, changeset: changeset) + end + + def update(conn = %{assigns: %{feed: feed}}, params = %{"feed" => feed_params}) do + changeset = Feed.update_changeset(feed, feed_params) + + case Repo.update(changeset) do + {:ok, feed} -> + params = replace_next_edit_path(params, ~p"/~/feeds/#{feed}/edit") + + conn + |> put_flash(:success, "Your feed has been updated! ✨") + |> redirect_next(params, ~p"/~/feeds") + + {:error, changeset} -> + conn + |> put_flash(:error, "There was a problem updating your feed. 😭") + |> render(:edit, feed: feed, changeset: changeset) + end + end + + def delete(conn = %{assigns: %{feed: feed}}, _params) do + Repo.delete!(feed) + + conn + |> put_flash(:success, "Your feed has been put out to pasture. 🐑") + |> redirect(to: ~p"/~/feeds") + end + + def refresh(conn = %{assigns: %{feed: feed}}, _params) do + FeedUpdater.queue(feed) + + conn + |> put_flash(:success, "Your feed is being rebuilt as we speak. 🥂") + |> redirect(to: ~p"/~/feeds") + end + + defp preload_current_user_extras(conn = %{assigns: %{current_user: me}}, _) do + me = + me + |> Repo.preload(:sponsors) + |> Repo.preload(:active_membership) + + assign(conn, :current_user, me) + end + + defp assign_podcasts(conn, _params) do + podcasts = + Podcast.active() + |> Podcast.by_position() + |> Repo.all() + + assign(conn, :podcasts, podcasts) + end + + defp assign_feed(conn = %{params: %{"id" => id}}, _params) do + feed = Feed |> Repo.get(id) |> Feed.preload_all() + assign(conn, :feed, feed) + end +end diff --git a/lib/changelog_web/controllers/home_controller.ex b/lib/changelog_web/controllers/home/home_controller.ex similarity index 100% rename from lib/changelog_web/controllers/home_controller.ex rename to lib/changelog_web/controllers/home/home_controller.ex diff --git a/lib/changelog_web/router.ex b/lib/changelog_web/router.ex index 49521f2d5a..129295f228 100644 --- a/lib/changelog_web/router.ex +++ b/lib/changelog_web/router.ex @@ -196,6 +196,8 @@ defmodule ChangelogWeb.Router do post "/~/subscribe", HomeController, :subscribe post "/~/unsubscribe", HomeController, :unsubscribe + resources "/~/feeds", Home.FeedController + get "/in", AuthController, :new, as: :sign_in post "/in", AuthController, :new, as: :sign_in get "/in/:token", AuthController, :create, as: :sign_in diff --git a/lib/changelog_web/templates/home/_nav.html.eex b/lib/changelog_web/templates/home/_nav.html.eex deleted file mode 100644 index 3dc542d116..0000000000 --- a/lib/changelog_web/templates/home/_nav.html.eex +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/lib/changelog_web/templates/home/_nav.html.heex b/lib/changelog_web/templates/home/_nav.html.heex new file mode 100644 index 0000000000..23da720a4b --- /dev/null +++ b/lib/changelog_web/templates/home/_nav.html.heex @@ -0,0 +1,25 @@ + diff --git a/lib/changelog_web/templates/home/feed/_form.html.heex b/lib/changelog_web/templates/home/feed/_form.html.heex new file mode 100644 index 0000000000..580efe9e3a --- /dev/null +++ b/lib/changelog_web/templates/home/feed/_form.html.heex @@ -0,0 +1,98 @@ +<%= form_for @changeset, @action, [class: "form", multipart: true], fn f -> %> +
+
+ + <%= text_input(f, :name, placeholder: "Changelog++") %> + <%= PublicHelpers.error_message(f, :name) %> +
+ +
+ + <%= text_input(f, :description, placeholder: "It's better!") %> + <%= PublicHelpers.error_message(f, :description) %> +

Add a summary or tagline to your feed.

+
+
+ +
+
+ + <%= hidden_input(f, :cover, value: nil) %> + <%= file_input(f, :cover) %> + <%= PublicHelpers.error_message(f, :cover) %> +

Ideal size is 3000px by 3000px.

+ <%= SharedHelpers.maybe_lazy_image(@conn, PodcastView.cover_url(@changeset.data, :medium), @changeset.data.name, width: 67, height: 67, style: "position: absolute; top: 0; right: 0;") %> +
+
+ + + +
+
+ +
+
+ + <%= text_input(f, :title_format, placeholder: "{title} {subtitle} ({podcast} #\{number\})") %> + <%= PublicHelpers.error_message(f, :title_format) %> +

Leave this empty if you just want regular episode titles. You can use these fields: title, subtitle, podcast, number

+
+ +
+ + <%= date_input(f, :starts_at) %> + <%= PublicHelpers.error_message(f, :starts_at) %> +

Feed will only include episodes published after this date. Leave blank to include all episodes.

+
+
+ + +
+ <%= for podcast <- @podcasts do %> +
+ <%= if Enum.member?(f.data.podcast_ids, podcast.id) do %> + + <% else %> + + <% end %> +
+ <% end %> +
+

Select which pods you want to include in this feed.

+ +
+
+ +
+ +
+ +
+
+ +
+

+
+ +
+
+
+<% end %> diff --git a/lib/changelog_web/templates/home/feed/edit.html.heex b/lib/changelog_web/templates/home/feed/edit.html.heex new file mode 100644 index 0000000000..770a5f45fd --- /dev/null +++ b/lib/changelog_web/templates/home/feed/edit.html.heex @@ -0,0 +1,6 @@ +
+ <%= render(HomeView, "_nav.html", assigns) %> +

Editing <%= @feed.name %>

+ + <%= render("_form.html", Map.merge(assigns, %{action: ~p"/~/feeds/#{@feed}"})) %> +
diff --git a/lib/changelog_web/templates/home/feed/index.html.heex b/lib/changelog_web/templates/home/feed/index.html.heex new file mode 100644 index 0000000000..2a761d72bd --- /dev/null +++ b/lib/changelog_web/templates/home/feed/index.html.heex @@ -0,0 +1,42 @@ +
+ <%= render(HomeView, "_nav.html", assigns) %> +
+ <%= link("Add Feed", to: ~p"/~/feeds/new", class: "button") %> +
+ +
+ + + + + + + + + + + <%= for feed <- @feeds do %> + + + + + + <% end %> + +
FeedIncludesActions
+ +
+ <%= feed.name %> +
+ <%= for podcast <- @podcasts do %> + <%= if Enum.member?(feed.podcast_ids, podcast.id) do %> + + <% end %> + <% end %> + + <%= link("Edit", to: ~p"/~/feeds/#{feed}/edit") %> + <%= link("Refresh", to: ~p"/~/feeds/#{feed}/edit") %> + <%= link("Delete", to: ~p"/~/feeds/#{feed}", method: :delete, data: [confirm: "Are you sure?"]) %> +
+
+
diff --git a/lib/changelog_web/templates/home/feed/new.html.heex b/lib/changelog_web/templates/home/feed/new.html.heex new file mode 100644 index 0000000000..95699c7241 --- /dev/null +++ b/lib/changelog_web/templates/home/feed/new.html.heex @@ -0,0 +1,6 @@ +
+ <%= render(HomeView, "_nav.html", assigns) %> +

New Feed

+ + <%= render("_form.html", Map.merge(assigns, %{action: ~p"/~/feeds"})) %> +
diff --git a/lib/changelog_web/templates/home/show.html.heex b/lib/changelog_web/templates/home/show.html.heex index 3ffb941adf..fe974145b4 100644 --- a/lib/changelog_web/templates/home/show.html.heex +++ b/lib/changelog_web/templates/home/show.html.heex @@ -9,10 +9,11 @@ <%= if @current_user.active_membership do %>

It's better!

-

Thank you for supporting our work with your hard-earned cash! Coming Soon: You'll be able to create and manage custom feeds right here on changelog.com.

+

Thank you for supporting our work with your hard-earned cash! Try it out: You can now create and manage your own custom feeds right here on changelog.com.

<%= link("Launch Supercast", to: "https://changelog.com/++", target: "_blank") %> + <%= link("Manage Feeds", to: ~p"/~/feeds") %>
<% else %>

Join Changelog++

diff --git a/lib/changelog_web/views/home/feed_view.ex b/lib/changelog_web/views/home/feed_view.ex new file mode 100644 index 0000000000..c41663bb13 --- /dev/null +++ b/lib/changelog_web/views/home/feed_view.ex @@ -0,0 +1,6 @@ +defmodule ChangelogWeb.Home.FeedView do + use ChangelogWeb, :public_view + + alias Changelog.{Podcast} + alias ChangelogWeb.{HomeView, PodcastView} +end