mirror of
https://github.com/algora-io/tv.git
synced 2025-02-14 01:59:50 +02:00
add shows (#40)
This commit is contained in:
parent
5fa6e1805c
commit
72acdbe1f1
@ -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
|
Loading…
x
Reference in New Issue
Block a user