You've already forked algora-tv
							
							
				mirror of
				https://github.com/algora-io/tv.git
				synced 2025-10-30 23:07:56 +02:00 
			
		
		
		
	add shows (#40)
This commit is contained in:
		| @@ -11,6 +11,7 @@ defmodule Algora.Accounts.User do | ||||
|     field :channel_tagline, :string | ||||
|     field :avatar_url, :string | ||||
|     field :external_homepage_url, :string | ||||
|     field :twitter_url, :string | ||||
|     field :videos_count, :integer | ||||
|     field :is_live, :boolean, default: false | ||||
|     field :stream_key, :string | ||||
|   | ||||
| @@ -53,7 +53,8 @@ defmodule Algora.Application do | ||||
|         id: Membrane.RTMP.Source.TcpServer, | ||||
|         start: {Membrane.RTMP.Source.TcpServer, :start_link, [tcp_server_options]} | ||||
|       }, | ||||
|       Algora.Stargazer | ||||
|       Algora.Stargazer, | ||||
|       ExMarcel.TableWrapper | ||||
|       # Start a worker by calling: Algora.Worker.start_link(arg) | ||||
|       # {Algora.Worker, arg} | ||||
|     ] | ||||
|   | ||||
							
								
								
									
										164
									
								
								lib/algora/events.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								lib/algora/events.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| defmodule Algora.Events do | ||||
|   import Ecto.Query | ||||
|  | ||||
|   alias Algora.{Repo} | ||||
|   alias Algora.Library.Video | ||||
|   alias Algora.Events.Event | ||||
|   alias Algora.Accounts.{User, Identity} | ||||
|  | ||||
|   def toggle_subscription_event(user, show) do | ||||
|     name = if subscribed?(user, show), do: :unsubscribed, else: :subscribed | ||||
|  | ||||
|     %Event{ | ||||
|       actor_id: "user_#{user.id}", | ||||
|       user_id: user.id, | ||||
|       show_id: show.id, | ||||
|       channel_id: show.user_id, | ||||
|       name: name | ||||
|     } | ||||
|     |> Event.changeset(%{}) | ||||
|     |> Repo.insert() | ||||
|   end | ||||
|  | ||||
|   def toggle_rsvp_event(user, show) do | ||||
|     name = if rsvpd?(user, show), do: :unrsvpd, else: :rsvpd | ||||
|  | ||||
|     %Event{ | ||||
|       actor_id: "user_#{user.id}", | ||||
|       user_id: user.id, | ||||
|       show_id: show.id, | ||||
|       channel_id: show.user_id, | ||||
|       name: name | ||||
|     } | ||||
|     |> Event.changeset(%{}) | ||||
|     |> Repo.insert() | ||||
|   end | ||||
|  | ||||
|   def subscribed?(nil, _show), do: false | ||||
|  | ||||
|   def subscribed?(user, show) do | ||||
|     event = | ||||
|       from( | ||||
|         e in Event, | ||||
|         where: | ||||
|           e.channel_id == ^show.user_id and | ||||
|             e.user_id == ^user.id and | ||||
|             (e.name == :subscribed or | ||||
|                e.name == :unsubscribed), | ||||
|         order_by: [desc: e.inserted_at], | ||||
|         limit: 1 | ||||
|       ) | ||||
|       |> Repo.one() | ||||
|  | ||||
|     event && event.name == :subscribed | ||||
|   end | ||||
|  | ||||
|   def rsvpd?(nil, _show), do: false | ||||
|  | ||||
|   def rsvpd?(user, show) do | ||||
|     event = | ||||
|       from( | ||||
|         e in Event, | ||||
|         where: | ||||
|           e.channel_id == ^show.user_id and | ||||
|             e.user_id == ^user.id and | ||||
|             (e.name == :rsvpd or | ||||
|                e.name == :unrsvpd), | ||||
|         order_by: [desc: e.inserted_at], | ||||
|         limit: 1 | ||||
|       ) | ||||
|       |> Repo.one() | ||||
|  | ||||
|     event && event.name == :rsvpd | ||||
|   end | ||||
|  | ||||
|   def fetch_attendees(show) do | ||||
|     # Get the latest relevant events (:rsvpd and :unrsvpd) for each user | ||||
|     latest_events_query = | ||||
|       from(e in Event, | ||||
|         where: e.channel_id == ^show.user_id and e.name in [:rsvpd, :unrsvpd], | ||||
|         order_by: [desc: e.inserted_at], | ||||
|         distinct: e.user_id | ||||
|       ) | ||||
|  | ||||
|     # Join user data and filter for :rsvpd events | ||||
|     from(e in subquery(latest_events_query), | ||||
|       join: u in User, | ||||
|       on: e.user_id == u.id, | ||||
|       join: i in Identity, | ||||
|       on: i.user_id == u.id and i.provider == "github", | ||||
|       select_merge: %{ | ||||
|         user_handle: u.handle, | ||||
|         user_display_name: coalesce(u.name, u.handle), | ||||
|         user_email: u.email, | ||||
|         user_avatar_url: u.avatar_url, | ||||
|         user_github_handle: i.provider_login | ||||
|       }, | ||||
|       where: e.name == :rsvpd, | ||||
|       order_by: [desc: e.inserted_at, desc: e.id] | ||||
|     ) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def fetch_unique_viewers(user) do | ||||
|     subquery_first_watched = | ||||
|       from(e in Event, | ||||
|         where: e.channel_id == ^user.id and e.name in [:watched, :subscribed], | ||||
|         order_by: [asc: e.inserted_at], | ||||
|         distinct: e.user_id | ||||
|       ) | ||||
|  | ||||
|     from(e in subquery(subquery_first_watched), | ||||
|       join: u in User, | ||||
|       on: e.user_id == u.id, | ||||
|       join: i in Identity, | ||||
|       on: i.user_id == u.id and i.provider == "github", | ||||
|       left_join: v in Video, | ||||
|       on: e.video_id == v.id, | ||||
|       select_merge: %{ | ||||
|         user_handle: u.handle, | ||||
|         user_display_name: coalesce(u.name, u.handle), | ||||
|         user_email: u.email, | ||||
|         user_avatar_url: u.avatar_url, | ||||
|         user_github_handle: i.provider_login, | ||||
|         first_video_id: e.video_id, | ||||
|         first_video_title: v.title | ||||
|       }, | ||||
|       distinct: e.user_id, | ||||
|       order_by: [desc: e.inserted_at, desc: e.id] | ||||
|     ) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def fetch_unique_subscribers(user) do | ||||
|     # Get the latest relevant events (:subscribed and :unsubscribed) for each user | ||||
|     latest_events_query = | ||||
|       from(e in Event, | ||||
|         where: e.channel_id == ^user.id and e.name in [:subscribed, :unsubscribed], | ||||
|         order_by: [desc: e.inserted_at], | ||||
|         distinct: e.user_id | ||||
|       ) | ||||
|  | ||||
|     # Join user data and filter for :subscribed events | ||||
|     from(e in subquery(latest_events_query), | ||||
|       join: u in User, | ||||
|       on: e.user_id == u.id, | ||||
|       join: i in Identity, | ||||
|       on: i.user_id == u.id and i.provider == "github", | ||||
|       left_join: v in Video, | ||||
|       on: e.video_id == v.id, | ||||
|       select_merge: %{ | ||||
|         user_handle: u.handle, | ||||
|         user_display_name: coalesce(u.name, u.handle), | ||||
|         user_email: u.email, | ||||
|         user_avatar_url: u.avatar_url, | ||||
|         user_github_handle: i.provider_login, | ||||
|         first_video_id: e.video_id, | ||||
|         first_video_title: v.title | ||||
|       }, | ||||
|       where: e.name == :subscribed, | ||||
|       order_by: [desc: e.inserted_at, desc: e.id] | ||||
|     ) | ||||
|     |> Repo.all() | ||||
|   end | ||||
| end | ||||
| @@ -7,6 +7,7 @@ defmodule Algora.Events.Event do | ||||
|     field :user_id, :integer | ||||
|     field :video_id, :integer | ||||
|     field :channel_id, :integer | ||||
|     field :show_id, :integer | ||||
|     field :user_handle, :string, virtual: true | ||||
|     field :user_display_name, :string, virtual: true | ||||
|     field :user_email, :string, virtual: true | ||||
| @@ -14,7 +15,7 @@ defmodule Algora.Events.Event do | ||||
|     field :user_github_handle, :string, virtual: true | ||||
|     field :first_video_id, :integer, virtual: true | ||||
|     field :first_video_title, :string, virtual: true | ||||
|     field :name, Ecto.Enum, values: [:subscribed, :unsubscribed, :watched] | ||||
|     field :name, Ecto.Enum, values: [:subscribed, :unsubscribed, :watched, :rsvpd, :unrsvpd] | ||||
|  | ||||
|     timestamps() | ||||
|   end | ||||
|   | ||||
| @@ -658,6 +658,7 @@ defmodule Algora.Library do | ||||
|       tagline: user.channel_tagline, | ||||
|       avatar_url: user.avatar_url, | ||||
|       external_homepage_url: user.external_homepage_url, | ||||
|       twitter_url: user.twitter_url, | ||||
|       is_live: user.is_live, | ||||
|       bounties_count: user.bounties_count, | ||||
|       orgs_contributed: user.orgs_contributed, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ defmodule Algora.Library.Channel do | ||||
|             tagline: nil, | ||||
|             avatar_url: nil, | ||||
|             external_homepage_url: nil, | ||||
|             twitter_url: nil, | ||||
|             is_live: nil, | ||||
|             bounties_count: nil, | ||||
|             orgs_contributed: nil, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ defmodule Algora.Library.Video do | ||||
|  | ||||
|   alias Algora.Accounts.User | ||||
|   alias Algora.Library.Video | ||||
|   alias Algora.Shows.Show | ||||
|   alias Algora.Chat.Message | ||||
|  | ||||
|   @type t() :: %__MODULE__{} | ||||
| @@ -32,6 +33,7 @@ defmodule Algora.Library.Video do | ||||
|     field :local_path, :string | ||||
|  | ||||
|     belongs_to :user, User | ||||
|     belongs_to :show, Show | ||||
|     belongs_to :transmuxed_from, Video | ||||
|  | ||||
|     has_many :messages, Message | ||||
|   | ||||
							
								
								
									
										34
									
								
								lib/algora/shows.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/algora/shows.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| defmodule Algora.Shows do | ||||
|   import Ecto.Query, warn: false | ||||
|   alias Algora.Repo | ||||
|  | ||||
|   alias Algora.Shows.Show | ||||
|  | ||||
|   def list_shows do | ||||
|     Repo.all(Show) | ||||
|   end | ||||
|  | ||||
|   def get_show!(id), do: Repo.get!(Show, id) | ||||
|  | ||||
|   def get_show_by_fields!(fields), do: Repo.get_by!(Show, fields) | ||||
|  | ||||
|   def create_show(attrs \\ %{}) do | ||||
|     %Show{} | ||||
|     |> Show.changeset(attrs) | ||||
|     |> Repo.insert() | ||||
|   end | ||||
|  | ||||
|   def update_show(%Show{} = show, attrs) do | ||||
|     show | ||||
|     |> Show.changeset(attrs) | ||||
|     |> Repo.update() | ||||
|   end | ||||
|  | ||||
|   def delete_show(%Show{} = show) do | ||||
|     Repo.delete(show) | ||||
|   end | ||||
|  | ||||
|   def change_show(%Show{} = show, attrs \\ %{}) do | ||||
|     Show.changeset(show, attrs) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										27
									
								
								lib/algora/shows/show.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/algora/shows/show.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| defmodule Algora.Shows.Show do | ||||
|   use Ecto.Schema | ||||
|   import Ecto.Changeset | ||||
|  | ||||
|   alias Algora.Accounts.User | ||||
|  | ||||
|   schema "shows" do | ||||
|     field :title, :string | ||||
|     field :description, :string | ||||
|     field :slug, :string | ||||
|     field :scheduled_for, :naive_datetime | ||||
|     field :image_url, :string | ||||
|     field :og_image_url, :string | ||||
|     field :url, :string | ||||
|  | ||||
|     belongs_to :user, User | ||||
|  | ||||
|     timestamps() | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def changeset(show, attrs) do | ||||
|     show | ||||
|     |> cast(attrs, [:title, :description, :slug, :scheduled_for, :image_url, :url]) | ||||
|     |> validate_required([:title, :slug]) | ||||
|   end | ||||
| end | ||||
| @@ -166,16 +166,14 @@ defmodule AlgoraWeb.CoreComponents do | ||||
|  | ||||
|   def playlist(assigns) do | ||||
|     ~H""" | ||||
|     <div> | ||||
|       <div class="align-middle inline-block min-w-full"> | ||||
|         <div id={@id} class="px-4 min-w-full"> | ||||
|           <div | ||||
|             id={"#{@id}-body"} | ||||
|             class="mt-3 gap-8 grid sm:grid-cols-2 lg:grid-cols-3" | ||||
|             phx-update="stream" | ||||
|           > | ||||
|             <.video_entry :for={{_id, video} <- @videos} video={video} /> | ||||
|           </div> | ||||
|     <div class="align-middle inline-block min-w-full"> | ||||
|       <div id={@id} class="px-4 min-w-full"> | ||||
|         <div | ||||
|           id={"#{@id}-body"} | ||||
|           class="mt-3 gap-8 grid sm:grid-cols-2 lg:grid-cols-3" | ||||
|           phx-update="stream" | ||||
|         > | ||||
|           <.video_entry :for={{_id, video} <- @videos} video={video} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @@ -498,7 +496,7 @@ defmodule AlgoraWeb.CoreComponents do | ||||
|                 <button | ||||
|                   phx-click={hide_modal(@on_cancel, @id)} | ||||
|                   type="button" | ||||
|                   class="-m-3 flex-none p-3 opacity-20 hover:opacity-40" | ||||
|                   class="-m-3 flex-none p-3 opacity-20 hover:opacity-40 focus:outline-none focus:opacity-40" | ||||
|                   aria-label={gettext("close")} | ||||
|                 > | ||||
|                   <Heroicons.x_mark solid class="w-5 h-5" /> | ||||
|   | ||||
| @@ -35,6 +35,24 @@ defmodule AlgoraWeb.UserAuth do | ||||
|     Ecto.NoResultsError -> {:halt, redirect_require_login(socket)} | ||||
|   end | ||||
|  | ||||
|   def on_mount(:ensure_admin, _params, session, socket) do | ||||
|     case session do | ||||
|       %{"user_id" => user_id} -> | ||||
|         user = Accounts.get_user!(user_id) | ||||
|  | ||||
|         if Accounts.admin?(user) do | ||||
|           {:cont, socket} | ||||
|         else | ||||
|           {:halt, LiveView.redirect(socket, to: ~p"/status/404")} | ||||
|         end | ||||
|  | ||||
|       %{} -> | ||||
|         {:halt, redirect_require_login(socket)} | ||||
|     end | ||||
|   rescue | ||||
|     Ecto.NoResultsError -> {:halt, redirect_require_login(socket)} | ||||
|   end | ||||
|  | ||||
|   defp redirect_require_login(socket) do | ||||
|     socket | ||||
|     |> LiveView.put_flash(:error, "Please sign in") | ||||
|   | ||||
| @@ -1,11 +1,7 @@ | ||||
| defmodule AlgoraWeb.AudienceLive do | ||||
|   use AlgoraWeb, :live_view | ||||
|   import Ecto.Query, warn: false | ||||
|  | ||||
|   alias Algora.Accounts.{User, Identity} | ||||
|   alias Algora.Events.Event | ||||
|   alias Algora.Library.Video | ||||
|   alias Algora.Repo | ||||
|   alias Algora.Events | ||||
|  | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
| @@ -60,6 +56,7 @@ defmodule AlgoraWeb.AudienceLive do | ||||
|         </:col> | ||||
|         <:col :let={subscriber}> | ||||
|           <.link | ||||
|             :if={subscriber.first_video_id} | ||||
|             navigate={~p"/#{@current_user.handle}/#{subscriber.first_video_id}"} | ||||
|             class="truncate ml-auto flex w-[200px]" | ||||
|           > | ||||
| @@ -100,6 +97,7 @@ defmodule AlgoraWeb.AudienceLive do | ||||
|         </:col> | ||||
|         <:col :let={viewer}> | ||||
|           <.link | ||||
|             :if={viewer.first_video_id} | ||||
|             navigate={~p"/#{@current_user.handle}/#{viewer.first_video_id}"} | ||||
|             class="truncate ml-auto flex w-[200px]" | ||||
|           > | ||||
| @@ -115,71 +113,12 @@ defmodule AlgoraWeb.AudienceLive do | ||||
|  | ||||
|   def mount(_params, _session, socket) do | ||||
|     user = socket.assigns.current_user | ||||
|     viewers = fetch_unique_viewers(user) | ||||
|     subscribers = fetch_unique_subscribers(user) | ||||
|     viewers = Events.fetch_unique_viewers(user) | ||||
|     subscribers = Events.fetch_unique_subscribers(user) | ||||
|  | ||||
|     {:ok, assign(socket, viewers: viewers, subscribers: subscribers)} | ||||
|   end | ||||
|  | ||||
|   defp fetch_unique_viewers(user) do | ||||
|     subquery_first_watched = | ||||
|       from(e in Event, | ||||
|         where: e.channel_id == ^user.id and e.name in [:watched, :subscribed], | ||||
|         order_by: [asc: e.inserted_at], | ||||
|         distinct: e.user_id | ||||
|       ) | ||||
|  | ||||
|     from(e in subquery(subquery_first_watched), | ||||
|       join: u in User, | ||||
|       on: e.user_id == u.id, | ||||
|       join: i in Identity, | ||||
|       on: i.user_id == u.id and i.provider == "github", | ||||
|       join: v in Video, | ||||
|       on: e.video_id == v.id, | ||||
|       select_merge: %{ | ||||
|         user_handle: u.handle, | ||||
|         user_display_name: coalesce(u.name, u.handle), | ||||
|         user_email: u.email, | ||||
|         user_avatar_url: u.avatar_url, | ||||
|         user_github_handle: i.provider_login, | ||||
|         first_video_id: e.video_id, | ||||
|         first_video_title: v.title | ||||
|       }, | ||||
|       distinct: e.user_id, | ||||
|       order_by: [desc: e.inserted_at, desc: e.id] | ||||
|     ) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   defp fetch_unique_subscribers(user) do | ||||
|     # Get the latest relevant events (:subscribed and :unsubscribed) for each user | ||||
|     latest_events_query = | ||||
|       from(e in Event, | ||||
|         where: e.channel_id == ^user.id and e.name in [:subscribed, :unsubscribed], | ||||
|         order_by: [desc: e.inserted_at], | ||||
|         distinct: e.user_id | ||||
|       ) | ||||
|  | ||||
|     # Join user data and filter for :subscribed events | ||||
|     from(e in subquery(latest_events_query), | ||||
|       join: u in User, | ||||
|       on: e.user_id == u.id, | ||||
|       join: i in Identity, | ||||
|       on: i.user_id == u.id and i.provider == "github", | ||||
|       join: v in Video, | ||||
|       on: e.video_id == v.id, | ||||
|       select_merge: %{ | ||||
|         user_handle: u.handle, | ||||
|         user_display_name: coalesce(u.name, u.handle), | ||||
|         user_email: u.email, | ||||
|         user_avatar_url: u.avatar_url, | ||||
|         user_github_handle: i.provider_login, | ||||
|         first_video_id: e.video_id, | ||||
|         first_video_title: v.title | ||||
|       }, | ||||
|       where: e.name == :subscribed, | ||||
|       order_by: [desc: e.inserted_at, desc: e.id] | ||||
|     ) | ||||
|     |> Repo.all() | ||||
|     {:ok, | ||||
|      socket | ||||
|      |> assign(:viewers, viewers) | ||||
|      |> assign(:subscribers, subscribers)} | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										180
									
								
								lib/algora_web/live/show_live/form_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								lib/algora_web/live/show_live/form_component.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| defmodule AlgoraWeb.ShowLive.FormComponent do | ||||
|   use AlgoraWeb, :live_component | ||||
|  | ||||
|   alias Algora.Shows | ||||
|  | ||||
|   @impl true | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
|     <div> | ||||
|       <.header class="pb-6"> | ||||
|         <%= @title %> | ||||
|       </.header> | ||||
|  | ||||
|       <.simple_form | ||||
|         for={@form} | ||||
|         id="show-form" | ||||
|         phx-target={@myself} | ||||
|         phx-change="validate" | ||||
|         phx-submit="save" | ||||
|       > | ||||
|         <div class="flex flex-col md:flex-row gap-8 justify-between"> | ||||
|           <div class="w-full space-y-8"> | ||||
|             <.input field={@form[:title]} type="text" label="Title" /> | ||||
|             <.input field={@form[:description]} type="textarea" label="Description" rows={3} /> | ||||
|           </div> | ||||
|           <div class="shrink-0"> | ||||
|             <label for="show_title" class="block text-sm font-semibold leading-6 text-gray-100 mb-2"> | ||||
|               Cover image | ||||
|             </label> | ||||
|             <div id="show_image" phx-drop-target={@uploads.cover_image.ref} class="relative"> | ||||
|               <.live_file_input | ||||
|                 upload={@uploads.cover_image} | ||||
|                 class="absolute inset-0 opacity-0 cursor-pointer" | ||||
|               /> | ||||
|               <img :if={@show.image_url} src={@show.image_url} class="max-w-[200px] rounded-lg" /> | ||||
|               <div :if={!@show.image_url} class="w-[200px] h-[200px] bg-white/10 rounded-lg"></div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="relative"> | ||||
|           <div class="absolute text-sm start-0 flex items-center ps-3 top-10 mt-px pointer-events-none text-gray-400"> | ||||
|             tv.algora.io/shows/ | ||||
|           </div> | ||||
|           <.input | ||||
|             field={@form[:slug]} | ||||
|             type="text" | ||||
|             label="URL" | ||||
|             placeholder="showname" | ||||
|             class="ps-[8.25rem]" | ||||
|           /> | ||||
|         </div> | ||||
|         <.input field={@form[:scheduled_for]} type="datetime-local" label="Date (UTC)" /> | ||||
|         <%!-- <.input field={@form[:image_url]} type="text" label="Image URL" /> --%> | ||||
|         <%= for err <- upload_errors(@uploads.cover_image) do %> | ||||
|           <p class="alert alert-danger"><%= error_to_string(err) %></p> | ||||
|         <% end %> | ||||
|         <:actions> | ||||
|           <.button phx-disable-with="Saving...">Save</.button> | ||||
|         </:actions> | ||||
|       </.simple_form> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def mount(socket) do | ||||
|     {:ok, | ||||
|      socket | ||||
|      |> assign(:uploaded_files, []) | ||||
|      |> allow_upload(:cover_image, | ||||
|        accept: accept(), | ||||
|        max_file_size: max_file_size() * 1_000_000, | ||||
|        max_entries: 1, | ||||
|        auto_upload: true, | ||||
|        progress: &handle_progress/3 | ||||
|      )} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def update(%{show: show} = assigns, socket) do | ||||
|     changeset = Shows.change_show(show) | ||||
|  | ||||
|     {:ok, | ||||
|      socket | ||||
|      |> assign(assigns) | ||||
|      |> assign_form(changeset)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("validate", %{"show" => show_params}, socket) do | ||||
|     changeset = | ||||
|       socket.assigns.show | ||||
|       |> Shows.change_show(show_params) | ||||
|       |> Map.put(:action, :validate) | ||||
|  | ||||
|     {:noreply, assign_form(socket, changeset)} | ||||
|   end | ||||
|  | ||||
|   def handle_event("save", %{"show" => show_params}, socket) do | ||||
|     save_show(socket, socket.assigns.action, show_params) | ||||
|   end | ||||
|  | ||||
|   def handle_event("cancel-upload", %{"ref" => ref}, socket) do | ||||
|     {:noreply, cancel_upload(socket, :cover_image, ref)} | ||||
|   end | ||||
|  | ||||
|   defp handle_progress(:cover_image, entry, socket) do | ||||
|     if entry.done? do | ||||
|       show = | ||||
|         consume_uploaded_entry(socket, entry, fn %{path: path} = _meta -> | ||||
|           remote_path = "shows/#{socket.assigns.show.id}/cover/#{System.os_time(:second)}" | ||||
|  | ||||
|           {:ok, _} = | ||||
|             Algora.Storage.upload_from_filename(path, remote_path, fn _ -> nil end, | ||||
|               content_type: "image/jpeg" | ||||
|             ) | ||||
|  | ||||
|           bucket = Algora.config([:buckets, :media]) | ||||
|           %{scheme: scheme, host: host} = Application.fetch_env!(:ex_aws, :s3) |> Enum.into(%{}) | ||||
|  | ||||
|           Shows.update_show(socket.assigns.show, %{ | ||||
|             image_url: "#{scheme}#{host}/#{bucket}/#{remote_path}" | ||||
|           }) | ||||
|         end) | ||||
|  | ||||
|       notify_parent({:saved, show}) | ||||
|  | ||||
|       {:noreply, socket |> assign(:show, show)} | ||||
|     else | ||||
|       {:noreply, socket} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp save_show(socket, :edit, show_params) do | ||||
|     case Shows.update_show(socket.assigns.show, show_params) do | ||||
|       {:ok, show} -> | ||||
|         notify_parent({:saved, show}) | ||||
|  | ||||
|         {:noreply, | ||||
|          socket | ||||
|          |> put_flash(:info, "Show updated successfully") | ||||
|          |> push_patch(to: ~p"/shows/#{show.slug}")} | ||||
|  | ||||
|       {:error, %Ecto.Changeset{} = changeset} -> | ||||
|         {:noreply, assign_form(socket, changeset)} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp save_show(socket, :new, show_params) do | ||||
|     case Shows.create_show(show_params) do | ||||
|       {:ok, show} -> | ||||
|         notify_parent({:saved, show}) | ||||
|  | ||||
|         {:noreply, | ||||
|          socket | ||||
|          |> put_flash(:info, "Show created successfully") | ||||
|          |> push_patch(to: ~p"/shows/#{show.slug}")} | ||||
|  | ||||
|       {:error, %Ecto.Changeset{} = changeset} -> | ||||
|         {:noreply, assign_form(socket, changeset)} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp assign_form(socket, %Ecto.Changeset{} = changeset) do | ||||
|     assign(socket, :form, to_form(changeset)) | ||||
|   end | ||||
|  | ||||
|   defp error_to_string(:too_large) do | ||||
|     "Only images up to #{max_file_size()} MB are allowed." | ||||
|   end | ||||
|  | ||||
|   defp error_to_string(:not_accepted) do | ||||
|     "Uploaded file is not a valid image. Only #{accept() |> Enum.intersperse(", ") |> Enum.join()} files are allowed." | ||||
|   end | ||||
|  | ||||
|   defp max_file_size, do: 10 | ||||
|   defp accept, do: ~w(.png .jpg .jpeg .gif) | ||||
|  | ||||
|   defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) | ||||
| end | ||||
							
								
								
									
										41
									
								
								lib/algora_web/live/show_live/index.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								lib/algora_web/live/show_live/index.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| defmodule AlgoraWeb.ShowLive.Index do | ||||
|   use AlgoraWeb, :live_view | ||||
|  | ||||
|   alias Algora.Shows | ||||
|   alias Algora.Shows.Show | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, stream(socket, :shows, Shows.list_shows())} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_params(params, _url, socket) do | ||||
|     {:noreply, apply_action(socket, socket.assigns.live_action, params)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info({AlgoraWeb.ShowLive.FormComponent, {:saved, show}}, socket) do | ||||
|     {:noreply, socket |> assign(:show, show)} | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :new, _params) do | ||||
|     socket | ||||
|     |> assign(:page_title, "New Show") | ||||
|     |> assign(:show, %Show{}) | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :index, _params) do | ||||
|     socket | ||||
|     |> assign(:page_title, "Listing Shows") | ||||
|     |> assign(:show, nil) | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("delete", %{"id" => id}, socket) do | ||||
|     show = Shows.get_show!(id) | ||||
|     {:ok, _} = Shows.delete_show(show) | ||||
|  | ||||
|     {:noreply, stream_delete(socket, :shows, show)} | ||||
|   end | ||||
| end | ||||
							
								
								
									
										49
									
								
								lib/algora_web/live/show_live/index.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/algora_web/live/show_live/index.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <.header> | ||||
|   Listing Shows | ||||
|   <:actions> | ||||
|     <.link patch={~p"/admin/shows/new"}> | ||||
|       <.button>New Show</.button> | ||||
|     </.link> | ||||
|   </:actions> | ||||
| </.header> | ||||
|  | ||||
| <.table | ||||
|   id="shows" | ||||
|   rows={@streams.shows} | ||||
|   row_click={fn {_id, show} -> JS.navigate(~p"/shows/#{show.slug}") end} | ||||
| > | ||||
|   <:col :let={{_id, show}} label="Title"><%= show.title %></:col> | ||||
|   <:col :let={{_id, show}} label="Slug"><%= show.slug %></:col> | ||||
|   <:col :let={{_id, show}} label="Scheduled for"><%= show.scheduled_for %></:col> | ||||
|   <:col :let={{_id, show}} label="Image url"><%= show.image_url %></:col> | ||||
|   <:action :let={{_id, show}}> | ||||
|     <div class="sr-only"> | ||||
|       <.link navigate={~p"/shows/#{show.slug}"}>Show</.link> | ||||
|     </div> | ||||
|     <.link patch={~p"/shows/#{show.slug}/edit"}>Edit</.link> | ||||
|   </:action> | ||||
|   <:action :let={{id, show}}> | ||||
|     <.link | ||||
|       phx-click={JS.push("delete", value: %{id: show.id}) |> hide("##{id}")} | ||||
|       data-confirm="Are you sure?" | ||||
|     > | ||||
|       Delete | ||||
|     </.link> | ||||
|   </:action> | ||||
| </.table> | ||||
|  | ||||
| <.modal | ||||
|   :if={@live_action in [:new, :edit]} | ||||
|   id="show-modal" | ||||
|   show | ||||
|   on_cancel={JS.patch(~p"/admin/shows")} | ||||
| > | ||||
|   <.live_component | ||||
|     module={AlgoraWeb.ShowLive.FormComponent} | ||||
|     id={@show.id || :new} | ||||
|     title={@page_title} | ||||
|     action={@live_action} | ||||
|     show={@show} | ||||
|     patch={~p"/admin/shows"} | ||||
|   /> | ||||
| </.modal> | ||||
							
								
								
									
										355
									
								
								lib/algora_web/live/show_live/show.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										355
									
								
								lib/algora_web/live/show_live/show.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,355 @@ | ||||
| defmodule AlgoraWeb.ShowLive.Show do | ||||
|   use AlgoraWeb, :live_view | ||||
|  | ||||
|   alias Algora.{Shows, Library, Events} | ||||
|   alias Algora.Accounts | ||||
|   alias AlgoraWeb.LayoutComponent | ||||
|  | ||||
|   @impl true | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
|     <div class="text-white min-h-screen p-8"> | ||||
|       <div class="flex flex-col md:grid md:grid-cols-3 gap-6"> | ||||
|         <div class="md:col-span-1 bg-white/5 ring-1 ring-white/15 rounded-lg p-6 space-y-6"> | ||||
|           <img src={@show.image_url} class="max-h-[250px] rounded-lg" /> | ||||
|           <div class="space-y-2"> | ||||
|             <div class="flex items-center space-x-2"> | ||||
|               <span class="relative ring-4 ring-[#15122c] flex h-10 w-10 shrink-0 overflow-hidden rounded-full"> | ||||
|                 <img | ||||
|                   class="aspect-square h-full w-full" | ||||
|                   alt={@channel.name} | ||||
|                   src={@channel.avatar_url} | ||||
|                 /> | ||||
|               </span> | ||||
|               <span class="font-bold"><%= @channel.name %></span> | ||||
|               <.link | ||||
|                 :if={@channel.twitter_url} | ||||
|                 target="_blank" | ||||
|                 rel="noopener noreferrer" | ||||
|                 href={@channel.twitter_url} | ||||
|               > | ||||
|                 <svg | ||||
|                   xmlns="http://www.w3.org/2000/svg" | ||||
|                   width="24" | ||||
|                   height="24" | ||||
|                   viewBox="0 0 24 24" | ||||
|                   fill="none" | ||||
|                   stroke="currentColor" | ||||
|                   stroke-width="2" | ||||
|                   stroke-linecap="round" | ||||
|                   stroke-linejoin="round" | ||||
|                   class="text-white" | ||||
|                 > | ||||
|                   <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 4l11.733 16h4.267l-11.733 -16z" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" /> | ||||
|                 </svg> | ||||
|               </.link> | ||||
|             </div> | ||||
|             <div :if={!@owns_show?} class="pt-2"> | ||||
|               <.button :if={@current_user} phx-click="toggle_subscription"> | ||||
|                 <%= if @subscribed? do %> | ||||
|                   Unsubscribe | ||||
|                 <% else %> | ||||
|                   Subscribe | ||||
|                 <% end %> | ||||
|               </.button> | ||||
|               <.button :if={!@current_user && @authorize_url}> | ||||
|                 <.link navigate={@authorize_url}> | ||||
|                   Subscribe | ||||
|                 </.link> | ||||
|               </.button> | ||||
|             </div> | ||||
|             <div :if={@owns_show?} class="pt-2"> | ||||
|               <.button> | ||||
|                 <.link patch={~p"/shows/#{@show.slug}/edit"}> | ||||
|                   Edit show | ||||
|                 </.link> | ||||
|               </.button> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div :if={@attendees_count > 0} class="border-t border-white/15 pt-6 space-y-4"> | ||||
|             <div> | ||||
|               <span class="font-medium"><%= @attendees_count %> Attending</span> | ||||
|             </div> | ||||
|             <div> | ||||
|               <div class="flex -space-x-1"> | ||||
|                 <span | ||||
|                   :for={attendee <- @attendees |> Enum.take(@max_attendee_avatars_count)} | ||||
|                   class="relative ring-4 ring-[#15122c] flex h-10 w-10 shrink-0 overflow-hidden rounded-full" | ||||
|                 > | ||||
|                   <img | ||||
|                     class="aspect-square h-full w-full" | ||||
|                     alt={attendee.user_display_name} | ||||
|                     src={attendee.user_avatar_url} | ||||
|                   /> | ||||
|                 </span> | ||||
|               </div> | ||||
|               <div class="mt-2"> | ||||
|                 <span | ||||
|                   :for={ | ||||
|                     {attendee, i} <- | ||||
|                       Enum.with_index(@attendees) |> Enum.take(@max_attendee_names_count) | ||||
|                   } | ||||
|                   class="font-medium" | ||||
|                 > | ||||
|                   <span :if={i != 0} class="-ml-1">, </span><span><%= attendee.user_display_name %></span> | ||||
|                 </span> | ||||
|                 <span :if={@attendees_count > @max_attendee_names_count} class="font-medium"> | ||||
|                   and | ||||
|                   <span :if={@attendees_count == @max_attendee_names_count + 1}> | ||||
|                     <%= @attendees |> Enum.at(@max_attendee_names_count) %> | ||||
|                   </span> | ||||
|                   <span :if={@attendees_count != @max_attendee_names_count + 1}> | ||||
|                     <%= @attendees_count - @max_attendee_names_count %> others | ||||
|                   </span> | ||||
|                 </span> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="md:col-span-2 space-y-6"> | ||||
|           <div class="bg-white/5 ring-1 ring-white/15 rounded-lg p-6 space-y-6"> | ||||
|             <div class="flex flex-col md:flex-row md:items-start md:justify-between gap-6"> | ||||
|               <div> | ||||
|                 <h1 class="text-4xl font-bold"><%= @show.title %></h1> | ||||
|                 <div :if={@show.description} class="mt-4 space-y-4"> | ||||
|                   <div> | ||||
|                     <h2 class="text-2xl font-bold">About</h2> | ||||
|                     <p class="typography whitespace-pre-wrap"><%= @show.description %></p> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|  | ||||
|               <div class="space-y-2 w-full max-w-xs md:max-w-sm"> | ||||
|                 <div :if={@show.scheduled_for} class="bg-gray-950/75 p-4 rounded-lg"> | ||||
|                   <div class="flex flex-col md:flex-wrap md:flex-row gap-2 md:items-center md:justify-between"> | ||||
|                     <div class="flex items-center space-x-4"> | ||||
|                       <svg | ||||
|                         xmlns="http://www.w3.org/2000/svg" | ||||
|                         width="24" | ||||
|                         height="24" | ||||
|                         viewBox="0 0 24 24" | ||||
|                         fill="none" | ||||
|                         stroke="currentColor" | ||||
|                         stroke-width="2" | ||||
|                         stroke-linecap="round" | ||||
|                         stroke-linejoin="round" | ||||
|                         class="h-6 w-6 text-green-300 shrink-0" | ||||
|                       > | ||||
|                         <path d="M8 2v4"></path> | ||||
|                         <path d="M16 2v4"></path> | ||||
|                         <rect width="18" height="18" x="3" y="4" rx="2"></rect> | ||||
|                         <path d="M3 10h18"></path> | ||||
|                       </svg> | ||||
|                       <div class="shrink-0"> | ||||
|                         <div class="text-sm font-semibold"> | ||||
|                           <%= @show.scheduled_for | ||||
|                           |> Timex.to_datetime("Etc/UTC") | ||||
|                           |> Timex.Timezone.convert("America/New_York") | ||||
|                           |> Timex.format!("{WDfull}, {Mshort} {D}") %> | ||||
|                         </div> | ||||
|                         <div class="text-sm"> | ||||
|                           <%= @show.scheduled_for | ||||
|                           |> Timex.to_datetime("Etc/UTC") | ||||
|                           |> Timex.Timezone.convert("America/New_York") | ||||
|                           |> Timex.format!("{h12}:{m} {am}, Eastern Time") %> | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     <.button :if={@current_user && !@rsvpd?} phx-click="toggle_rsvp"> | ||||
|                       Attend | ||||
|                     </.button> | ||||
|                     <.button | ||||
|                       :if={@rsvpd?} | ||||
|                       disabled | ||||
|                       class="bg-green-400 hover:bg-green-400 disabled:opacity-100 text-green-950 flex items-center active:text-green-950" | ||||
|                     > | ||||
|                       <svg | ||||
|                         xmlns="http://www.w3.org/2000/svg" | ||||
|                         width="24" | ||||
|                         height="24" | ||||
|                         viewBox="0 0 24 24" | ||||
|                         fill="none" | ||||
|                         stroke="currentColor" | ||||
|                         stroke-width="2" | ||||
|                         stroke-linecap="round" | ||||
|                         stroke-linejoin="round" | ||||
|                         class="h-5 w-5 -ml-0.5" | ||||
|                       > | ||||
|                         <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M5 12l5 5l10 -10" /> | ||||
|                       </svg> | ||||
|                       <span class="ml-1">Attending</span> | ||||
|                     </.button> | ||||
|                     <.button :if={!@current_user && @authorize_url}> | ||||
|                       <.link navigate={@authorize_url}> | ||||
|                         Attend | ||||
|                       </.link> | ||||
|                     </.button> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <.link | ||||
|                   href={@show.url || ~p"/#{@channel.handle}/latest"} | ||||
|                   target="_blank" | ||||
|                   rel="noopener" | ||||
|                   class="block bg-gray-950/75 p-4 rounded-lg" | ||||
|                 > | ||||
|                   <div class="flex items-center justify-between"> | ||||
|                     <div class="flex items-center space-x-4 shrink-0"> | ||||
|                       <svg | ||||
|                         xmlns="http://www.w3.org/2000/svg" | ||||
|                         width="24" | ||||
|                         height="24" | ||||
|                         viewBox="0 0 24 24" | ||||
|                         fill="none" | ||||
|                         stroke="currentColor" | ||||
|                         stroke-width="2" | ||||
|                         stroke-linecap="round" | ||||
|                         stroke-linejoin="round" | ||||
|                         class="h-6 w-6 text-red-400" | ||||
|                       > | ||||
|                         <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M18.364 19.364a9 9 0 1 0 -12.728 0" /><path d="M15.536 16.536a5 5 0 1 0 -7.072 0" /><path d="M12 13m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /> | ||||
|                       </svg> | ||||
|                       <div> | ||||
|                         <div class="text-sm font-semibold">Watch live</div> | ||||
|                         <div class="text-sm"> | ||||
|                           <%= @show.url || "tv.algora.io/#{@channel.handle}/latest" %> | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </.link> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="bg-white/5 ring-1 ring-white/15 rounded-lg p-6 pb-2 space-y-6"> | ||||
|             <h2 class="pt-4 text-2xl font-bold">Past sessions</h2> | ||||
|             <div | ||||
|               id="past-sessions" | ||||
|               class="flex gap-8 overflow-x-scroll pb-4 scrollbar-thin" | ||||
|               phx-update="stream" | ||||
|             > | ||||
|               <div :for={{_id, video} <- @streams.videos} class="max-w-xs shrink-0 w-full"> | ||||
|                 <.link class="cursor-pointer truncate" href={~p"/#{video.channel_handle}/#{video.id}"}> | ||||
|                   <.video_thumbnail video={video} class="rounded-2xl" /> | ||||
|                   <div class="pt-2 text-base font-semibold truncate"><%= video.title %></div> | ||||
|                   <div class="text-gray-300 text-sm"><%= Timex.from_now(video.inserted_at) %></div> | ||||
|                 </.link> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <.modal | ||||
|       :if={@live_action in [:new, :edit]} | ||||
|       id="show-modal" | ||||
|       show | ||||
|       on_cancel={JS.patch(~p"/shows/#{@show.slug}")} | ||||
|     > | ||||
|       <.live_component | ||||
|         module={AlgoraWeb.ShowLive.FormComponent} | ||||
|         id={@show.id || :new} | ||||
|         title={@page_title} | ||||
|         action={@live_action} | ||||
|         show={@show} | ||||
|       /> | ||||
|     </.modal> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def mount(%{"slug" => slug}, _session, socket) do | ||||
|     %{current_user: current_user} = socket.assigns | ||||
|  | ||||
|     show = Shows.get_show_by_fields!(slug: slug) | ||||
|  | ||||
|     channel = Accounts.get_user(show.user_id) |> Library.get_channel!() | ||||
|  | ||||
|     videos = Library.list_channel_videos(channel, 50) | ||||
|  | ||||
|     {:ok, | ||||
|      socket | ||||
|      |> assign_attendees(show) | ||||
|      |> assign(:show, show) | ||||
|      |> assign(:owns_show?, current_user && show.user_id == current_user.id) | ||||
|      |> assign(:channel, channel) | ||||
|      |> assign(:max_attendee_avatars_count, 5) | ||||
|      |> assign(:max_attendee_names_count, 2) | ||||
|      |> assign(:subscribed?, Events.subscribed?(current_user, channel)) | ||||
|      |> assign(:rsvpd?, Events.rsvpd?(current_user, channel)) | ||||
|      |> stream(:videos, videos)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_params(params, url, socket) do | ||||
|     %{path: path} = URI.parse(url) | ||||
|     LayoutComponent.hide_modal() | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> assign(:authorize_url, Algora.Github.authorize_url(path)) | ||||
|      |> apply_action(socket.assigns.live_action, params)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("toggle_subscription", _params, socket) do | ||||
|     Events.toggle_subscription_event(socket.assigns.current_user, socket.assigns.show) | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> assign(:subscribed?, !socket.assigns.subscribed?)} | ||||
|   end | ||||
|  | ||||
|   def handle_event("toggle_rsvp", _params, socket) do | ||||
|     Events.toggle_rsvp_event(socket.assigns.current_user, socket.assigns.show) | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> assign_attendees(socket.assigns.show) | ||||
|      |> assign(:rsvpd?, !socket.assigns.rsvpd?)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info({AlgoraWeb.ShowLive.FormComponent, {:saved, show}}, socket) do | ||||
|     {:noreply, socket |> assign(:show, show)} | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :show, %{"slug" => slug}) do | ||||
|     show = Shows.get_show_by_fields!(slug: slug) | ||||
|  | ||||
|     socket | ||||
|     |> assign(:page_title, show.title) | ||||
|     |> assign(:page_description, show.description) | ||||
|     |> assign(:page_image, show.og_image_url) | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :edit, %{"slug" => slug}) do | ||||
|     %{current_user: current_user} = socket.assigns | ||||
|  | ||||
|     show = Shows.get_show_by_fields!(slug: slug) | ||||
|  | ||||
|     cond do | ||||
|       current_user == nil -> | ||||
|         socket | ||||
|         |> redirect(to: ~p"/auth/login") | ||||
|  | ||||
|       current_user.id != show.user_id && !Accounts.admin?(current_user) -> | ||||
|         socket | ||||
|         |> put_flash(:error, "You don't have permission to edit this show") | ||||
|         |> redirect(to: ~p"/shows/#{show.slug}") | ||||
|  | ||||
|       true -> | ||||
|         socket | ||||
|         |> assign(:page_title, show.title) | ||||
|         |> assign(:page_description, show.description) | ||||
|         |> assign(:page_image, show.og_image_url) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp assign_attendees(socket, show) do | ||||
|     attendees = Events.fetch_attendees(show) | ||||
|  | ||||
|     socket | ||||
|     |> assign(:attendees, attendees) | ||||
|     |> assign(:attendees_count, length(attendees)) | ||||
|   end | ||||
| end | ||||
| @@ -767,6 +767,7 @@ defmodule AlgoraWeb.VideoLive do | ||||
|     {:noreply, socket |> assign(subscribed?: !socket.assigns.subscribed?)} | ||||
|   end | ||||
|  | ||||
|   # TODO: move into events context | ||||
|   defp toggle_subscription_event(user, video) do | ||||
|     name = if subscribed?(user, video), do: :unsubscribed, else: :subscribed | ||||
|  | ||||
| @@ -781,6 +782,7 @@ defmodule AlgoraWeb.VideoLive do | ||||
|     |> Repo.insert() | ||||
|   end | ||||
|  | ||||
|   # TODO: move into events context | ||||
|   defp subscribed?(nil, _video), do: false | ||||
|  | ||||
|   defp subscribed?(user, video) do | ||||
|   | ||||
| @@ -65,6 +65,16 @@ defmodule AlgoraWeb.Router do | ||||
|  | ||||
|     delete "/auth/logout", OAuthCallbackController, :sign_out | ||||
|  | ||||
|     live_session :admin, | ||||
|       on_mount: [ | ||||
|         {AlgoraWeb.UserAuth, :ensure_authenticated}, | ||||
|         {AlgoraWeb.UserAuth, :ensure_admin}, | ||||
|         AlgoraWeb.Nav | ||||
|       ] do | ||||
|       live "/shows", ShowLive.Index, :index | ||||
|       live "/shows/new", ShowLive.Index, :new | ||||
|     end | ||||
|  | ||||
|     live_session :authenticated, | ||||
|       on_mount: [{AlgoraWeb.UserAuth, :ensure_authenticated}, AlgoraWeb.Nav] do | ||||
|       live "/channel/settings", SettingsLive, :edit | ||||
| @@ -85,6 +95,10 @@ defmodule AlgoraWeb.Router do | ||||
|       live "/auth/login", SignInLive, :index | ||||
|       live "/cossgpt", COSSGPTLive, :index | ||||
|       live "/og/cossgpt", COSSGPTOGLive, :index | ||||
|  | ||||
|       live "/shows/:slug", ShowLive.Show, :show | ||||
|       live "/shows/:slug/edit", ShowLive.Show, :edit | ||||
|  | ||||
|       live "/:channel_handle", ChannelLive, :show | ||||
|       live "/:channel_handle/:video_id", VideoLive, :show | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								mix.exs
									
									
									
									
									
								
							| @@ -42,6 +42,7 @@ defmodule Algora.MixProject do | ||||
|       {:elixir_make, "~> 0.7.0", runtime: false}, | ||||
|       {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, | ||||
|       {:ex_m3u8, "~> 0.9.0"}, | ||||
|       {:ex_marcel, "~> 0.1.0"}, | ||||
|       {:exla, ">= 0.0.0"}, | ||||
|       {:exsync, "~> 0.2", only: :dev}, | ||||
|       {:ffmpex, "~> 0.10.0"}, | ||||
|   | ||||
							
								
								
									
										1
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								mix.lock
									
									
									
									
									
								
							| @@ -33,6 +33,7 @@ | ||||
|   "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, | ||||
|   "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, | ||||
|   "ex_m3u8": {:hex, :ex_m3u8, "0.9.0", "54a12463320236aab09402bc69676f665e692636235a2b186a22df507ebc5643", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d57939a90d8da5956264d27a516c5e2ac80b09c8adbe4e3199d7d14c79549b5c"}, | ||||
|   "ex_marcel": {:hex, :ex_marcel, "0.1.0", "61116a255ed51e8d9ec65cb0018442bf37cadb2228d3bb90e3bbb6d3d11dcd34", [:mix], [], "hexpm", "48dfc497435a9c52c0e90c1e07d8ce7316a095dcec0e04d182e8250e493b72fb"}, | ||||
|   "exla": {:hex, :exla, "0.7.0", "27fac40a580f0d3816fe3bf35c50dfc2f99597d26ac7e2aca4a3c62b89bb427f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.7.0", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.6.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "d3bfc622deb52cec95efc9d76063891afc7cd33e38eddbb01f3385c53e043c40"}, | ||||
|   "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, | ||||
|   "exsync": {:hex, :exsync, "0.3.0", "39ab8b3d4e5fe779a34ad930135145283ebf56069513dfdfaad4e30a04b158c7", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2030d085a14fa5f685d53d97171a21345dddaf2b67a0927263efc6b2cd2bb09f"}, | ||||
|   | ||||
							
								
								
									
										35
									
								
								priv/repo/migrations/20240524154930_create_shows.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								priv/repo/migrations/20240524154930_create_shows.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| defmodule Algora.Repo.Migrations.CreateShows do | ||||
|   use Ecto.Migration | ||||
|  | ||||
|   def change do | ||||
|     create table(:shows) do | ||||
|       add :title, :string, null: false | ||||
|       add :description, :string | ||||
|       add :slug, :citext, null: false | ||||
|       add :scheduled_for, :naive_datetime | ||||
|       add :image_url, :string | ||||
|       add :og_image_url, :string | ||||
|       add :url, :string | ||||
|       add :user_id, references(:users, on_delete: :nothing) | ||||
|  | ||||
|       timestamps() | ||||
|     end | ||||
|  | ||||
|     alter table(:events) do | ||||
|       add :show_id, references(:shows) | ||||
|     end | ||||
|  | ||||
|     alter table(:videos) do | ||||
|       add :show_id, references(:shows) | ||||
|     end | ||||
|  | ||||
|     alter table(:users) do | ||||
|       add :twitter_url, :string | ||||
|     end | ||||
|  | ||||
|     create unique_index(:shows, [:slug]) | ||||
|     create index(:shows, [:user_id]) | ||||
|     create index(:events, [:show_id]) | ||||
|     create index(:videos, [:show_id]) | ||||
|   end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user