Revamp homepage (#90)
@ -18,6 +18,7 @@ defmodule Algora.Accounts.User do
|
||||
field :visibility, Ecto.Enum, values: [public: 1, unlisted: 2]
|
||||
field :bounties_count, :integer
|
||||
field :solving_challenge, :boolean, default: false
|
||||
field :featured, :boolean, default: false
|
||||
|
||||
embeds_many :tech, Tech do
|
||||
field :name, :string
|
||||
|
@ -611,7 +611,7 @@ defmodule Algora.Library do
|
||||
}
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@ -627,7 +627,7 @@ defmodule Algora.Library do
|
||||
}
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@ -644,7 +644,7 @@ defmodule Algora.Library do
|
||||
}
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@ -686,7 +686,7 @@ defmodule Algora.Library do
|
||||
where: v.show_id in ^ids
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@ -706,7 +706,7 @@ defmodule Algora.Library do
|
||||
}
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@ -726,7 +726,7 @@ defmodule Algora.Library do
|
||||
v.user_id == ^channel.user_id
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@ -747,16 +747,35 @@ defmodule Algora.Library do
|
||||
v.user_id == ^channel.user_id
|
||||
)
|
||||
|> Video.not_deleted()
|
||||
|> order_by_inserted(:desc)
|
||||
|> order_by_live()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def list_active_channels(opts) do
|
||||
def list_live_channels(opts) do
|
||||
from(u in Algora.Accounts.User,
|
||||
where: u.is_live and u.visibility == :public,
|
||||
limit: ^Keyword.fetch!(opts, :limit),
|
||||
order_by: [desc: u.updated_at],
|
||||
select: struct(u, [:id, :handle, :channel_tagline, :avatar_url, :external_homepage_url])
|
||||
order_by: [desc: u.updated_at]
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Enum.map(&get_channel!/1)
|
||||
end
|
||||
|
||||
def list_active_channels(opts) do
|
||||
limit = Keyword.fetch!(opts, :limit)
|
||||
|
||||
from(u in Algora.Accounts.User,
|
||||
left_join:
|
||||
v in subquery(
|
||||
from v in Algora.Library.Video,
|
||||
where: is_nil(v.deleted_at),
|
||||
group_by: v.user_id,
|
||||
select: %{user_id: v.user_id, last_video_at: max(v.inserted_at)}
|
||||
),
|
||||
on: u.id == v.user_id,
|
||||
where: u.visibility == :public and (u.featured == true or u.is_live == true),
|
||||
order_by: [desc: u.is_live, desc: v.last_video_at, desc: u.id],
|
||||
limit: ^limit
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Enum.map(&get_channel!/1)
|
||||
@ -816,8 +835,8 @@ defmodule Algora.Library do
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp order_by_inserted(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do
|
||||
from(s in query, order_by: [{^direction, s.inserted_at}])
|
||||
defp order_by_live(%Ecto.Query{} = query) do
|
||||
from(s in query, order_by: [desc: s.is_live, desc: s.inserted_at, desc: s.id])
|
||||
end
|
||||
|
||||
defp topic(user_id) when is_integer(user_id), do: "channel:#{user_id}"
|
||||
|
@ -136,7 +136,7 @@ defmodule AlgoraWeb.CoreComponents do
|
||||
<%!-- HACK: should use navigate instead of href here --%>
|
||||
<%!-- but it breaks navigating from youtube video to another video --%>
|
||||
<.link class="cursor-pointer truncate" href={~p"/#{@video.channel_handle}/#{@video.id}"}>
|
||||
<.short_thumbnail video={@video} class="rounded-2xl" />
|
||||
<.short_thumbnail video={@video} class="rounded-lg" />
|
||||
<div class="pt-2 text-base font-semibold truncate"><%= @video.title %></div>
|
||||
<div class="text-gray-300 text-sm font-medium"><%= @video.channel_name %></div>
|
||||
<div class="text-gray-300 text-sm"><%= Timex.from_now(@video.inserted_at) %></div>
|
||||
@ -151,7 +151,7 @@ defmodule AlgoraWeb.CoreComponents do
|
||||
<%!-- HACK: should use navigate instead of href here --%>
|
||||
<%!-- but it breaks navigating from youtube video to another video --%>
|
||||
<.link class="cursor-pointer truncate" href={~p"/#{@video.channel_handle}/#{@video.id}"}>
|
||||
<.video_thumbnail video={@video} class="rounded-2xl" />
|
||||
<.video_thumbnail video={@video} class="rounded-lg" />
|
||||
<div class="pt-2 text-base font-semibold truncate"><%= @video.title %></div>
|
||||
<div class="text-gray-300 text-sm font-medium"><%= @video.channel_name %></div>
|
||||
<div class="text-gray-300 text-sm"><%= Timex.from_now(@video.inserted_at) %></div>
|
||||
|
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 flex items-center px-4">
|
||||
<.logo />
|
||||
<.logo class="w-16 h-auto hidden sm:flex" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex-1 h-0 overflow-y-auto">
|
||||
|
@ -62,28 +62,6 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fixed inset-0 bg-[radial-gradient(ellipse_at_top_left,_#1d1e3a_0%,_#050217_40%,_#050217_60%,_#1d1e3a_100%)] z-0">
|
||||
</div>
|
||||
<div
|
||||
class="fixed inset-x-0 -top-[69%] z-0 flex transform justify-center overflow-hidden blur-3xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="w-screen h-[150vh] flex-none bg-gradient-to-b from-gray-900 to-gray-950 opacity-75 transition-opacity"
|
||||
style="clip-path: polygon(70.71% 100%, 100% 70.71%, 100% 0%, 70.71% 0%, 29.29% 0%, 0% 0%, 0% 70.71%, 29.29% 100%)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="fixed inset-x-0 -top-[5%] z-0 flex transform justify-center overflow-hidden blur-3xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="w-screen h-[100vh] flex-none bg-gradient-to-r from-purple-900 to-violet-950 opacity-10 transition-opacity"
|
||||
style="clip-path: polygon(5.5% 13%, 52.75% 0%, 100% 13%, 100% 100%, 0% 100%)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative mt-[64px]">
|
||||
<div
|
||||
id="navbar"
|
||||
@ -200,10 +178,8 @@
|
||||
<% else %>
|
||||
<.link
|
||||
navigate="/auth/login"
|
||||
class="flex rounded px-4 py-2 overflow-hidden group bg-purple-500 relative hover:bg-gradient-to-r hover:from-purple-500 hover:to-purple-400 text-white hover:ring-2 hover:ring-offset-2 hover:ring-purple-400 transition-all ease-out duration-300"
|
||||
class="rounded-lg bg-gray-50 hover:bg-gray-200 py-1 px-2 text-sm font-semibold leading-6 text-gray-950 active:text-gray-950/80"
|
||||
>
|
||||
<span class="absolute right-0 w-8 h-32 -mt-12 transition-all duration-1000 transform translate-x-12 bg-white opacity-10 rotate-12 group-hover:-translate-x-40 ease">
|
||||
</span>
|
||||
<span class="relative font-semibold text-sm">Login</span>
|
||||
</.link>
|
||||
<% end %>
|
||||
|
55
lib/algora_web/live/hero_component.ex
Normal file
@ -0,0 +1,55 @@
|
||||
defmodule AlgoraWeb.HeroComponent do
|
||||
use AlgoraWeb, :live_component
|
||||
|
||||
alias Algora.{Library, Events}
|
||||
alias AlgoraWeb.Presence
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<video
|
||||
id={@id}
|
||||
phx-hook="VideoPlayer"
|
||||
class="video-js vjs-default-skin aspect-video h-full w-full flex-1 overflow-hidden"
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
# TODO: log at regular intervals
|
||||
# if socket.current_user && socket.assigns.video.is_live do
|
||||
# schedule_watch_event(:timer.seconds(2))
|
||||
# end
|
||||
|
||||
socket =
|
||||
case assigns[:video] do
|
||||
nil ->
|
||||
socket
|
||||
|
||||
video ->
|
||||
%{current_user: current_user} = assigns
|
||||
|
||||
Events.log_watched(current_user, video)
|
||||
|
||||
Presence.track_user(video.channel_handle, %{
|
||||
id: if(current_user, do: current_user.handle, else: "")
|
||||
})
|
||||
|
||||
socket
|
||||
|> push_event("play_video", %{
|
||||
player_id: assigns.id,
|
||||
id: video.id,
|
||||
url: video.url,
|
||||
title: video.title,
|
||||
player_type: Library.player_type(video),
|
||||
channel_name: video.channel_name,
|
||||
current_time: assigns[:current_time]
|
||||
})
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:id, assigns[:id])}
|
||||
end
|
||||
end
|
@ -1,252 +1,334 @@
|
||||
defmodule AlgoraWeb.HomeLive do
|
||||
use AlgoraWeb, :live_view
|
||||
|
||||
alias Algora.{Library, Shows}
|
||||
alias AlgoraWeb.PlayerComponent
|
||||
alias Algora.Library
|
||||
alias AlgoraWeb.HeroComponent
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto pt-2 pb-6 px-4 sm:px-6 space-y-6">
|
||||
<div class="-mt-12">
|
||||
<div class="mx-auto">
|
||||
<div class="mx-auto mt-16 max-w-2xl rounded-3xl bg-white/5 ring-2 ring-purple-500 sm:mt-20 lg:mx-0 lg:flex lg:max-w-none lg:items-center">
|
||||
<div class="p-8 sm:p-10 lg:flex-auto">
|
||||
<h3 class="text-3xl font-bold tracking-tight text-white">
|
||||
✨ New feature: Live Billboards!
|
||||
</h3>
|
||||
<p class="mt-6 font-medium text-lg leading-7 text-gray-300">
|
||||
We just launched in-video ads to help developers earn money while livestreaming and give devtools companies a channel to reach new audiences.
|
||||
</p>
|
||||
<div class="mt-6 flex items-center gap-4">
|
||||
<.button>
|
||||
<.link navigate={~p"/partner"}>
|
||||
Learn more
|
||||
</.link>
|
||||
</.button>
|
||||
<.button>
|
||||
<div>
|
||||
<!-- Static sidebar for desktop -->
|
||||
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-[28rem] lg:flex-col">
|
||||
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
||||
<div class="relative flex grow flex-col gap-y-5 overflow-y-auto scrollbar-thin bg-gray-950 px-4 py-6">
|
||||
<nav class="mt-8 flex flex-1 flex-col">
|
||||
<ul role="list" class="space-y-3">
|
||||
<%= for channel <- @channels do %>
|
||||
<li class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden">
|
||||
<.link
|
||||
href="https://www.youtube.com/watch?v=te6k6EfHjnI"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
navigate={channel_path(channel)}
|
||||
class="flex-1 flex items-center justify-between truncate gap-3"
|
||||
>
|
||||
Watch demo
|
||||
<img
|
||||
class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-full bg-purple-300"
|
||||
src={channel.avatar_url}
|
||||
alt={channel.handle}
|
||||
/>
|
||||
<div class="flex-1 flex items-center justify-between text-gray-50 text-sm hover:text-gray-300 truncate">
|
||||
<div class="flex-1 py-1 text-sm truncate">
|
||||
<div class="font-semibold truncate"><%= channel.name %></div>
|
||||
<div class="font-medium truncate"><%= channel.tagline %></div>
|
||||
</div>
|
||||
</div>
|
||||
<%= if channel.is_live do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2.5 h-2.5 bg-red-500 rounded-full" aria-hidden="true" />
|
||||
<span class="text-sm font-medium">Live</span>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">Offline</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</.link>
|
||||
</.button>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="lg:pl-[28rem] relative">
|
||||
<div id="navbar" phx-hook="NavBar" class="h-[56px] fixed z-[100] top-0 left-0 right-0 w-full">
|
||||
<div class="flex justify-between items-center my-auto h-full">
|
||||
<div class="flex items-center h-full w-[28rem] bg-gray-950">
|
||||
<.logo class="pl-4 mt-1 w-24 h-auto" />
|
||||
</div>
|
||||
<div class="-mt-2 p-8 pt-0 sm:p-10 sm:pt-0 lg:p-2 lg:mr-2 xl:mr-0 lg:mt-0 lg:w-full lg:max-w-lg xl:max-w-xl lg:flex-shrink-0 h-full">
|
||||
<.link
|
||||
class="cursor-pointer truncate"
|
||||
href="https://www.youtube.com/watch?v=te6k6EfHjnI"
|
||||
rel="noopener noreferrer"
|
||||
<div class="pr-4 h-full items-center justify-end gap-2 flex">
|
||||
<a
|
||||
class="group outline-none w-fit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://www.youtube.com/@algora-io"
|
||||
>
|
||||
<div class="relative flex items-center justify-center overflow-hidden aspect-[16/9] bg-gray-800 rounded-sm lg:rounded-3xl lg:rounded-l-none">
|
||||
<img
|
||||
src={~p"/images/live-billboard.png"}
|
||||
alt="Algora Live Billboards"
|
||||
class="absolute w-full h-full object-cover z-10"
|
||||
/>
|
||||
<div class="absolute font-medium text-xs px-2 py-0.5 rounded-xl bottom-1 bg-gray-950/90 text-white right-1 z-20">
|
||||
2:27
|
||||
<div class="text-center font-sans justify-center items-center shrink-0 transition duration-150 select-none group-focus:outline-none group-disabled:opacity-75 group-disabled:pointer-events-none bg-transparent hover:bg-slate-850 disabled:opacity-50 h-8 px-2 text-sm font-semibold rounded-[3px] whitespace-nowrap flex">
|
||||
<div class="justify-center flex w-full items-center gap-x-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="h-5 text-gray-300 transition mr-0.5 shrink-0 justify-start"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M18 3a5 5 0 0 1 5 5v8a5 5 0 0 1 -5 5h-12a5 5 0 0 1 -5 -5v-8a5 5 0 0 1 5 -5zm-9 6v6a1 1 0 0 0 1.514 .857l5 -3a1 1 0 0 0 0 -1.714l-5 -3a1 1 0 0 0 -1.514 .857z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.header class="pt-8">
|
||||
<h1 class="text-4xl font-semibold">Livestreaming for developers</h1>
|
||||
<p class="text-xl font-medium text-gray-200 italic">You'll never ship alone!</p>
|
||||
</.header>
|
||||
|
||||
<div :if={@livestream} class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<div class="w-full max-w-3xl">
|
||||
<.live_component module={PlayerComponent} id="home-player" />
|
||||
</div>
|
||||
<.link
|
||||
href={"/#{@livestream.channel_handle}/#{@livestream.id}"}
|
||||
class="w-full max-w-sm p-6 bg-gray-800/40 hover:bg-gray-800/60 overflow-hidden rounded-lg lg:rounded-2xl shadow-inner shadow-white/[10%] lg:border border-white/[15%] hover:border/white/[20%]"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative h-20 w-20 shrink-0">
|
||||
<img
|
||||
src={@livestream.channel_avatar_url}
|
||||
alt={@livestream.channel_handle}
|
||||
class="w-full h-full p-1 ring-4 rounded-full ring-red-500"
|
||||
/>
|
||||
<div class="absolute bottom-0 translate-y-1/2 ring-[3px] ring-gray-800 left-1/2 -translate-x-1/2 rounded px-1 font-medium mx-auto bg-red-500 text-xs">
|
||||
LIVE
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-3xl font-semibold"><%= @livestream.channel_name %></div>
|
||||
<div class="font-medium text-gray-300">@<%= @livestream.channel_handle %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4 font-medium text-gray-100"><%= @livestream.title %></div>
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div class="pt-12">
|
||||
<h2 class="text-white text-3xl font-semibold">
|
||||
Shows
|
||||
</h2>
|
||||
<ul role="list" class="pt-4 grid grid-cols-1 gap-12 sm:grid-cols-2">
|
||||
<li :for={show <- @shows} class="col-span-1">
|
||||
<div class="h-full flex flex-col rounded-2xl overflow-hidden bg-[#15112b] ring-1 ring-white/20 text-center shadow-lg relative group">
|
||||
<img
|
||||
class="object-cover absolute inset-0 shrink-0 h-[12rem] w-full bg-gray-950"
|
||||
src={show.image_url}
|
||||
alt=""
|
||||
/>
|
||||
<div class="absolute h-[12rem] w-full inset-0 bg-gradient-to-b from-transparent to-[#15112b]" />
|
||||
<.link navigate={~p"/shows/#{show.slug}"} class="absolute h-[10rem] w-full inset-0 z-10">
|
||||
</.link>
|
||||
<div class="relative text-left h-full">
|
||||
<div class="flex flex-1 flex-col h-full">
|
||||
<div class="px-4 mt-[8rem] flex-col sm:flex-row flex sm:items-center gap-4">
|
||||
<.link
|
||||
:if={show.channel_handle != "algora"}
|
||||
navigate={~p"/shows/#{show.slug}"}
|
||||
class="shrink-0"
|
||||
</a>
|
||||
<a class="group outline-none w-fit" target="_blank" href="https://algora.io/discord">
|
||||
<div class="text-center font-sans justify-center items-center shrink-0 transition duration-150 select-none group-focus:outline-none group-disabled:opacity-75 group-disabled:pointer-events-none bg-transparent hover:bg-slate-850 disabled:opacity-50 h-8 px-2 text-sm font-semibold rounded-[3px] whitespace-nowrap flex">
|
||||
<div class="justify-center flex w-full items-center gap-x-1">
|
||||
<svg
|
||||
class="h-7 w-7"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<img
|
||||
class="h-[8rem] w-[8rem] rounded-full ring-4 ring-white shrink-0"
|
||||
src={show.channel_avatar_url}
|
||||
alt=""
|
||||
/>
|
||||
</.link>
|
||||
<div :if={show.channel_handle == "algora"} class="h-[8rem] w-0 -ml-4"></div>
|
||||
<div>
|
||||
<.link navigate={~p"/shows/#{show.slug}"}>
|
||||
<h3 class="mt-auto text-3xl font-semibold text-white [text-shadow:#000_10px_5px_10px] line-clamp-2 hover:underline">
|
||||
<%= show.title %>
|
||||
</h3>
|
||||
</.link>
|
||||
<div :if={show.channel_handle != "algora"} class="flex items-center gap-2">
|
||||
<.link navigate={~p"/#{show.channel_handle}"}>
|
||||
<div class="text-base text-gray-300 font-semibold line-clamp-1 hover:underline">
|
||||
<%= show.channel_name %>
|
||||
</div>
|
||||
</.link>
|
||||
<.link
|
||||
:if={show.channel_twitter_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={show.channel_twitter_url}
|
||||
<path
|
||||
d="M18.0881 7.3374C18.0116 7.27279 17.9402 7.2032 17.8637 7.14356C17.554 6.88097 17.2269 6.63856 16.8846 6.41792C16.4342 6.13677 15.9516 5.90824 15.4464 5.73702C15.0844 5.61277 14.7172 5.51835 14.35 5.40901C14.2837 5.40901 14.2786 5.38414 14.3092 5.3245C14.3398 5.26485 14.4061 5.14558 14.4469 5.05115C14.4538 5.03366 14.4667 5.0191 14.4835 5.01001C14.5003 5.00092 14.5198 4.99789 14.5387 5.00146C14.809 5.04619 15.0844 5.07601 15.3547 5.13069C15.8281 5.229 16.2896 5.3756 16.7316 5.56805C17.1998 5.76225 17.6502 5.99501 18.0779 6.26385C18.2267 6.353 18.3697 6.45094 18.5063 6.5571C18.5891 6.62989 18.6566 6.71764 18.7051 6.81552C19.1108 7.51363 19.4521 8.24546 19.7251 9.00236C20.1066 10.0234 20.3983 11.0742 20.5971 12.1435C20.7042 12.715 20.7909 13.2866 20.8674 13.8631C20.9184 14.216 20.9388 14.5788 20.9745 14.9366C20.9745 15.0559 20.9745 15.1702 21 15.2895C21 15.3164 20.9911 15.3425 20.9745 15.3641C20.462 15.9257 19.8549 16.398 19.1794 16.7606C18.5379 17.1017 17.8516 17.3558 17.1395 17.5161C16.7511 17.6096 16.3554 17.6711 15.9564 17.7H15.7116C15.701 17.7002 15.6904 17.6981 15.6807 17.6938C15.671 17.6895 15.6624 17.6831 15.6555 17.6752C15.4413 17.4068 15.2323 17.1334 15.0232 16.8551V16.8253C16.3606 16.3823 17.5548 15.6041 18.4859 14.5689C18.3788 14.6434 18.2819 14.718 18.1748 14.7826C17.8739 14.9665 17.5781 15.1504 17.267 15.3193C16.7354 15.61 16.1728 15.8433 15.5892 16.0151C14.6422 16.3069 13.6595 16.474 12.6671 16.5121H12.3713H11.8155C11.4011 16.5146 10.9871 16.4897 10.5762 16.4376C10.1887 16.3879 9.80109 16.3332 9.41351 16.2636C8.86661 16.1567 8.33068 16.002 7.81221 15.8014C7.15233 15.5479 6.523 15.2246 5.93553 14.8372L5.55306 14.5788C6.01711 15.0934 6.54864 15.5462 7.13396 15.9257C7.72153 16.3044 8.35541 16.6099 9.02084 16.8352L8.98514 16.8899L8.39358 17.6553C8.38145 17.6729 8.36453 17.6868 8.34472 17.6956C8.3249 17.7044 8.30298 17.7076 8.28138 17.705C7.93875 17.691 7.59775 17.6511 7.26145 17.5857C6.76756 17.4952 6.28289 17.3621 5.81314 17.1881C5.27458 16.9934 4.76114 16.7382 4.28323 16.4277C3.86783 16.1551 3.48621 15.8365 3.14601 15.4784C3.14601 15.4784 3.12051 15.4386 3.10011 15.4287C3.06012 15.3983 3.03012 15.3571 3.01381 15.3103C2.9975 15.2635 2.99559 15.2131 3.00831 15.1653L3.05421 14.6335C3.0899 14.2856 3.1205 13.9426 3.1664 13.5947C3.2123 13.2468 3.28879 12.7647 3.36529 12.3472C3.51174 11.5311 3.7093 10.7244 3.95685 9.93177C4.16738 9.2543 4.42116 8.59033 4.71671 7.94373C4.91624 7.50667 5.14275 7.08178 5.39497 6.6714C5.46939 6.5728 5.56514 6.49137 5.67544 6.43284C6.1388 6.11857 6.63239 5.84893 7.14925 5.62769C7.71444 5.38251 8.30641 5.20075 8.91375 5.08594L9.47981 5.00643C9.49599 5.00328 9.51279 5.00611 9.52694 5.01438C9.54108 5.02265 9.55155 5.03575 9.55631 5.05115L9.7042 5.33942C9.7297 5.38415 9.7042 5.39907 9.6685 5.40901C9.41351 5.47859 9.15854 5.54319 8.90865 5.61774C8.45618 5.75584 8.01886 5.93729 7.60313 6.15946C7.24627 6.34465 6.9052 6.5574 6.58319 6.79565C6.3588 6.9696 6.14462 7.14853 5.92533 7.32745C5.9235 7.33135 5.92255 7.33557 5.92255 7.33986C5.92255 7.34415 5.9235 7.3484 5.92533 7.35229L5.99163 7.32248C6.471 7.09882 6.95037 6.86522 7.43994 6.65647C8.00719 6.4106 8.59831 6.22081 9.20443 6.08991C9.61682 5.99062 10.0361 5.92083 10.459 5.88114C10.8414 5.84635 11.2239 5.82649 11.6013 5.80661C11.79 5.80661 11.9787 5.80661 12.1673 5.80661C12.5141 5.80661 12.866 5.8414 13.2128 5.86625C13.8437 5.91322 14.4686 6.01806 15.0793 6.17936C15.6332 6.32264 16.1739 6.51049 16.6959 6.74099L17.9606 7.33243L18.0218 7.36224L18.0881 7.3374ZM9.35232 10.5679C9.08643 10.5761 8.82881 10.66 8.6113 10.8093C8.39378 10.9586 8.2259 11.1667 8.12839 11.4079C7.98657 11.7022 7.93351 12.0296 7.97541 12.3522C8.01397 12.7406 8.19505 13.1024 8.48538 13.371C8.61754 13.5006 8.77761 13.6 8.95401 13.6619C9.13041 13.7238 9.31872 13.7467 9.50531 13.7289C9.68475 13.7178 9.85988 13.6705 10.0196 13.5901C10.1794 13.5097 10.3203 13.3979 10.4335 13.2617C10.7252 12.9245 10.8682 12.4886 10.8312 12.049C10.8196 11.7253 10.7096 11.4122 10.515 11.1494C10.3862 10.9659 10.2123 10.8166 10.0093 10.7151C9.80628 10.6135 9.58046 10.563 9.35232 10.5679ZM16.1094 12.1733C16.1148 11.8593 16.0319 11.55 15.8697 11.2787C15.7548 11.0583 15.5775 10.8747 15.3587 10.7496C15.14 10.6245 14.889 10.5632 14.6356 10.5729C14.451 10.578 14.2698 10.6219 14.1043 10.7017C13.9388 10.7815 13.793 10.8953 13.6769 11.0351C13.5285 11.203 13.4159 11.398 13.3459 11.6088C13.2758 11.8196 13.2496 12.0419 13.2689 12.2627C13.2861 12.6947 13.4787 13.1023 13.8043 13.3959C13.9417 13.5243 14.1072 13.6205 14.2883 13.6773C14.4694 13.7342 14.6614 13.7501 14.8498 13.7239C15.1962 13.6764 15.5095 13.4978 15.7218 13.2269C15.9694 12.9284 16.106 12.5571 16.1094 12.1733Z"
|
||||
fill="#CBD5E1"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
:if={Algora.Stargazer.count()}
|
||||
class="group outline-none w-fit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/algora-io/tv"
|
||||
>
|
||||
<div class="text-center font-sans justify-center items-center shrink-0 transition duration-150 select-none group-focus:outline-none group-disabled:opacity-75 group-disabled:pointer-events-none bg-transparent hover:bg-slate-850 disabled:opacity-50 h-8 text-sm font-semibold rounded-[3px] whitespace-nowrap p-2 flex">
|
||||
<div class="justify-center flex w-full items-center gap-x-1">
|
||||
<svg
|
||||
class="h-5 text-gray-300 transition mr-0.5 shrink-0 justify-start"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_571_3822)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 0C5.37017 0 0 5.50708 0 12.306C0 17.745 3.44015 22.3532 8.20626 23.9849C8.80295 24.0982 9.02394 23.7205 9.02394 23.3881C9.02394 23.0935 9.01657 22.3229 9.00921 21.2956C5.67219 22.0359 4.96501 19.6487 4.96501 19.6487C4.41989 18.2285 3.63168 17.8508 3.63168 17.8508C2.54144 17.0878 3.71271 17.1029 3.71271 17.1029C4.91344 17.1936 5.55433 18.372 5.55433 18.372C6.62247 20.2531 8.36096 19.7092 9.04604 19.3919C9.15654 18.5987 9.46593 18.0548 9.80479 17.745C7.13812 17.4353 4.33886 16.3777 4.33886 11.6638C4.33886 10.3192 4.80295 9.2238 5.57643 8.36261C5.4512 8.05288 5.03867 6.79887 5.69429 5.1067C5.69429 5.1067 6.7035 4.77432 8.99447 6.36827C9.95212 6.09632 10.9761 5.96034 12 5.95279C13.0166 5.95279 14.0479 6.09632 15.0055 6.36827C17.2965 4.77432 18.3057 5.1067 18.3057 5.1067C18.9613 6.79887 18.5488 8.05288 18.4236 8.36261C19.1897 9.2238 19.6538 10.3192 19.6538 11.6638C19.6538 16.3928 16.8471 17.4278 14.1731 17.7375C14.6004 18.1152 14.9908 18.8706 14.9908 20.0189C14.9908 21.6657 14.9761 22.9877 14.9761 23.3957C14.9761 23.728 15.1897 24.1058 15.8011 23.9849C20.5672 22.3532 24 17.745 24 12.3135C24 5.50708 18.6298 0 12 0Z"
|
||||
fill="currentColor"
|
||||
>
|
||||
<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={show.channel_handle == "algora"} class="h-[24px]"></div>
|
||||
</div>
|
||||
</path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_571_3822">
|
||||
<rect width="24" height="24" fill="currentColor"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="hidden xl:block">Star</span>
|
||||
<span class="font-semibold text-gray-300"><%= Algora.Stargazer.count() %></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<.link
|
||||
class="px-4 py-2 sm:flex hidden whitespace-nowrap"
|
||||
target="_blank"
|
||||
href="/docs/streaming/quickstart"
|
||||
>
|
||||
<span class="relative font-semibold text-sm">How to livestream</span>
|
||||
</.link>
|
||||
<%= if @current_user do %>
|
||||
<div class="shrink-0">
|
||||
<.simple_dropdown id="navbar-account-dropdown">
|
||||
<:img src={@current_user.avatar_url} alt={@current_user.handle} />
|
||||
<:link navigate={channel_path(@current_user)}>Channel</:link>
|
||||
<:link navigate={~p"/channel/studio"}>Studio</:link>
|
||||
<:link navigate={~p"/subscriptions"}>Subscriptions</:link>
|
||||
<:link navigate={~p"/channel/settings"}>Settings</:link>
|
||||
<:link href={~p"/auth/logout"} method={:delete}>Sign out</:link>
|
||||
</.simple_dropdown>
|
||||
</div>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate="/auth/login"
|
||||
class="rounded-lg bg-gray-50 hover:bg-gray-200 py-1 px-2 text-sm font-semibold leading-6 text-gray-950 active:text-gray-950/80"
|
||||
>
|
||||
<span class="relative font-semibold text-sm">Login</span>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto pb-6">
|
||||
<.link
|
||||
:if={@hero_video}
|
||||
href={~p"/#{@hero_video.channel_handle}/#{@hero_video.id}"}
|
||||
class="flex h-full min-h-[100svh] sm:min-h-0"
|
||||
>
|
||||
<div class="w-full my-auto relative">
|
||||
<.live_component module={HeroComponent} id="home-player" />
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-gray-950/80 to-transparent to-50%">
|
||||
</div>
|
||||
<div class="hidden sm:block absolute inset-0 bg-gradient-to-b from-gray-950 to-transparent to-30%">
|
||||
</div>
|
||||
<div class="absolute my-auto top-1/2 -translate-y-1/2 left-8 w-1/2 sm:truncate">
|
||||
<div
|
||||
:if={@hero_video.is_live}
|
||||
class="pl-2 mb-2 text-white bg-red-500 rounded-xl font-semibold inline-flex items-center py-0.5"
|
||||
>
|
||||
LIVE
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-4xl sm:text-7xl font-bold [text-shadow:#020617_1px_0_10px]">
|
||||
<%= @hero_video.channel_name %>
|
||||
</div>
|
||||
<div class="pt-2 text-lg sm:text-xl [text-shadow:#020617_1px_0_10px] font-medium sm:truncate">
|
||||
<%= @hero_video.title %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<div class="pt-8 px-4 lg:pl-0 lg:pr-8 gap-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<.video_entry :for={video <- @most_recent_videos} video={video} />
|
||||
</div>
|
||||
|
||||
<div class="px-4 lg:pl-0 lg:pr-8 pt-12">
|
||||
<h2 class="text-white text-3xl font-semibold">
|
||||
Shows
|
||||
</h2>
|
||||
<ul role="list" class="pt-4 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<%= for show <- @shows do %>
|
||||
<li class="col-span-1 rounded-lg overflow-hidden">
|
||||
<div>
|
||||
<.link
|
||||
:if={show.scheduled_for}
|
||||
navigate={~p"/shows/#{show.slug}"}
|
||||
class="shrink-0 sm:hidden xl:flex bg-gray-900 px-3 py-2 rounded-lg ring-1 ring-green-300 mr-auto sm:mr-0 sm:ml-auto flex items-center space-x-2"
|
||||
class="aspect-h-1 aspect-w-1 w-full overflow-hidden bg-gray-200 group-hover:opacity-75"
|
||||
>
|
||||
<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") %>
|
||||
<img src={show.poster} alt={show.slug} class="h-full w-full" />
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/#{show.channel_handle}"}
|
||||
class="bg-gray-900 p-2 flex justify-between"
|
||||
>
|
||||
<div class="flex-1 flex items-center justify-between truncate gap-3">
|
||||
<img
|
||||
class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-full bg-purple-300"
|
||||
src={show.channel_avatar_url}
|
||||
alt={show.channel_name}
|
||||
/>
|
||||
<div class="flex-1 flex items-center justify-between text-gray-50 text-sm hover:text-gray-300">
|
||||
<div class="flex-1 py-1 text-sm">
|
||||
<div class="font-semibold whitespace-normal">
|
||||
<%= show.channel_name %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={length(Enum.filter(@show_eps, fn v -> v.show_id == show.id end)) > 0}
|
||||
class="mt-auto pt-[2rem] -mb-2"
|
||||
>
|
||||
<div class="flex justify-between items-center gap-2 px-2">
|
||||
<h3 class="text-sm uppercase text-gray-300 font-semibold">Past episodes</h3>
|
||||
</div>
|
||||
<div class="p-2 flex gap-4 overflow-x-scroll scrollbar-thin transition-all">
|
||||
<div
|
||||
:for={video <- Enum.filter(@show_eps, fn v -> v.show_id == show.id end)}
|
||||
class="max-w-[12rem] sm:max-w-[16rem] shrink-0 w-full"
|
||||
>
|
||||
<.link class="truncate" href={~p"/#{video.channel_handle}/#{video.id}"}>
|
||||
<.video_thumbnail video={video} class="rounded-xl" />
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 lg:pl-0 lg:pr-8 pt-12">
|
||||
<h2 class="text-white text-3xl font-semibold">
|
||||
Past livestreams
|
||||
</h2>
|
||||
<div class="pt-4 gap-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<.video_entry :for={video <- @rest_of_videos} video={video} />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="pt-12">
|
||||
<h2 class="text-white text-3xl font-semibold">
|
||||
Most recent livestreams
|
||||
</h2>
|
||||
<div class="pt-4 gap-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<.video_entry :for={video <- @videos} video={video} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
shows = Shows.list_featured_shows()
|
||||
show_eps = shows |> Enum.map(fn s -> s.id end) |> Library.list_videos_by_show_ids()
|
||||
active_channels = Library.list_active_channels(limit: 20)
|
||||
videos = Library.list_videos(150)
|
||||
livestream = Library.list_livestreams(1) |> Enum.at(0)
|
||||
|
||||
[hero_video | recent_videos] = videos
|
||||
{most_recent_videos, rest_of_videos} = Enum.split(recent_videos, 3)
|
||||
|
||||
shows = [
|
||||
%{
|
||||
slug: "tsperf",
|
||||
poster: ~p"/images/shows/coding-challenges.jpg",
|
||||
channel_name: "Algora",
|
||||
channel_handle: "algora",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/136125894?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "coss",
|
||||
poster: ~p"/images/shows/coss-office-hours.jpg",
|
||||
channel_name: "Peer Richelsen",
|
||||
channel_handle: "PeerRich",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/8019099?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "the_savefile",
|
||||
poster: ~p"/images/shows/the-save-file.jpg",
|
||||
channel_name: "Glauber Costa",
|
||||
channel_handle: "glommer",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/331197?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "rfc",
|
||||
poster: ~p"/images/shows/request-for-comments.jpg",
|
||||
channel_name: "Andreas Klinger",
|
||||
channel_handle: "rfc",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/245833?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "coss-founder-podcast",
|
||||
poster: ~p"/images/shows/coss-founder-podcast.jpg",
|
||||
channel_name: "Ioannis R. Florokapis",
|
||||
channel_handle: "algora",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/118012453?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "bounties",
|
||||
poster: ~p"/images/shows/live-bounty-hunting.jpg",
|
||||
channel_name: "Algora",
|
||||
channel_handle: "algora",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/136125894?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "eu-acc",
|
||||
poster: ~p"/images/shows/eu-acc.jpg",
|
||||
channel_name: "Andreas Klinger",
|
||||
channel_handle: "rfc",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/245833?v=4"
|
||||
},
|
||||
%{
|
||||
slug: "buildinpublic",
|
||||
poster: ~p"/images/shows/build-in-public.jpg",
|
||||
channel_name: "Algora",
|
||||
channel_handle: "algora",
|
||||
channel_avatar_url: "https://avatars.githubusercontent.com/u/136125894?v=4"
|
||||
}
|
||||
]
|
||||
|
||||
if connected?(socket) do
|
||||
Library.subscribe_to_livestreams()
|
||||
|
||||
if livestream do
|
||||
send_update(PlayerComponent, %{
|
||||
if hero_video do
|
||||
send_update(HeroComponent, %{
|
||||
id: "home-player",
|
||||
video: livestream,
|
||||
video: hero_video,
|
||||
current_user: socket.assigns.current_user
|
||||
})
|
||||
end
|
||||
@ -254,10 +336,11 @@ defmodule AlgoraWeb.HomeLive do
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:show_eps, show_eps)
|
||||
|> assign(:videos, videos)
|
||||
|> assign(:most_recent_videos, most_recent_videos)
|
||||
|> assign(:rest_of_videos, rest_of_videos)
|
||||
|> assign(:shows, shows)
|
||||
|> assign(:livestream, livestream)}
|
||||
|> assign(:hero_video, hero_video)
|
||||
|> assign(:channels, active_channels)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@ -270,7 +353,7 @@ defmodule AlgoraWeb.HomeLive do
|
||||
{Library, %Library.Events.LivestreamStarted{video: %{visibility: :public} = video}},
|
||||
%{assigns: %{livestream: nil}} = socket
|
||||
) do
|
||||
send_update(PlayerComponent, %{
|
||||
send_update(HeroComponent, %{
|
||||
id: "home-player",
|
||||
video: video,
|
||||
current_user: socket.assigns.current_user
|
||||
|
@ -8,7 +8,7 @@ defmodule AlgoraWeb.Nav do
|
||||
def on_mount(:default, _params, _session, socket) do
|
||||
{:cont,
|
||||
socket
|
||||
|> assign(active_users: Library.list_active_channels(limit: 20))
|
||||
|> assign(active_users: Library.list_live_channels(limit: 20))
|
||||
|> assign(:region, System.get_env("FLY_REGION") || "iad")
|
||||
|> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)}
|
||||
end
|
||||
|
@ -58,7 +58,9 @@ defmodule AlgoraWeb.Router do
|
||||
|
||||
live_session :ads,
|
||||
layout: {AlgoraWeb.Layouts, :live_bare},
|
||||
root_layout: {AlgoraWeb.Layouts, :root_embed} do
|
||||
root_layout: {AlgoraWeb.Layouts, :root_embed},
|
||||
on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Nav] do
|
||||
live "/", HomeLive, :show
|
||||
live "/partner", PartnerLive, :show
|
||||
live "/:channel_handle/ads", AdOverlayLive, :show
|
||||
end
|
||||
@ -128,7 +130,6 @@ defmodule AlgoraWeb.Router do
|
||||
end
|
||||
|
||||
live_session :default, on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Nav] do
|
||||
live "/", HomeLive, :show
|
||||
live "/auth/login", SignInLive, :index
|
||||
live "/cossgpt", COSSGPTLive, :index
|
||||
live "/og/cossgpt", COSSGPTOGLive, :index
|
||||
|
@ -0,0 +1,9 @@
|
||||
defmodule Algora.Repo.Local.Migrations.AddFeaturedToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add :featured, :boolean, default: false
|
||||
end
|
||||
end
|
||||
end
|
@ -5,21 +5,19 @@
|
||||
alias Algora.{Repo, Accounts}
|
||||
alias Algora.Accounts.User
|
||||
alias Algora.Library.Video
|
||||
alias Algora.Shows.Show
|
||||
|
||||
user =
|
||||
case Accounts.get_user_by(handle: "algora") do
|
||||
nil ->
|
||||
{:ok, user} =
|
||||
Repo.insert(%User{
|
||||
handle: "algora",
|
||||
name: "Algora",
|
||||
avatar_url: "https://fly.storage.tigris.dev/algora/test/algora.png",
|
||||
email: "algora@example.com",
|
||||
visibility: :public,
|
||||
is_live: true
|
||||
})
|
||||
|
||||
user
|
||||
Repo.insert!(%User{
|
||||
handle: "algora",
|
||||
name: "Algora",
|
||||
avatar_url: "https://fly.storage.tigris.dev/algora/test/algora.png",
|
||||
email: "algora@example.com",
|
||||
visibility: :public,
|
||||
is_live: true
|
||||
})
|
||||
|
||||
existing_user ->
|
||||
existing_user
|
||||
@ -110,3 +108,159 @@ Repo.insert!(%Video{
|
||||
visibility: :public,
|
||||
uuid: Ecto.UUID.generate()
|
||||
})
|
||||
|
||||
[
|
||||
%{
|
||||
handle: "glommer",
|
||||
name: "Glauber Costa",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/331197?v=4",
|
||||
channel_tagline: "The Save File Ep. 15"
|
||||
},
|
||||
%{
|
||||
handle: "spirodonfl",
|
||||
name: "Spiro Floropoulos",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/314869?v=4",
|
||||
channel_tagline:
|
||||
"X_TECH_LEAD-- = Videogame work, content, Laravel/HTMX, so much #zig #webassembly"
|
||||
},
|
||||
%{
|
||||
handle: "heyandras",
|
||||
name: "Andras Bacsai",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/5845193?v=4",
|
||||
channel_tagline: "Hangout, coding & open-source"
|
||||
},
|
||||
%{
|
||||
handle: "danielroe",
|
||||
name: "Daniel Roe",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/28706372?v=4",
|
||||
channel_tagline: "🚦 nitro + nuxt ecosystem testing"
|
||||
},
|
||||
%{
|
||||
handle: "cmgriffing",
|
||||
name: "cmgriffing",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/1195435?v=4",
|
||||
channel_tagline: "🔐 Rolling my own auth: 2FA"
|
||||
},
|
||||
%{
|
||||
handle: "LLCoolChris_",
|
||||
name: "Christopher N. KATOYI",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/16650656?v=4",
|
||||
channel_tagline:
|
||||
"🥸 [FR/EN] 24H OCaml with Codecrafters & Exercism | Some Wukong Gaming | Stuff"
|
||||
},
|
||||
%{
|
||||
handle: "PeerRich",
|
||||
name: "Peer Richelsen",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/8019099?v=4",
|
||||
channel_tagline: "COSS Office Hours with @peer_rich from Cal.com"
|
||||
},
|
||||
%{
|
||||
handle: "rfc",
|
||||
name: "Andreas Klinger",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/245833?v=4",
|
||||
channel_tagline: "🇪🇺 Let's talk eu/acc! 🔴 LIVE - Chat @ rfc.to 🎉"
|
||||
},
|
||||
%{
|
||||
handle: "McPizza0",
|
||||
name: "McPizza",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/17185737?v=4",
|
||||
channel_tagline: "Working through some business features"
|
||||
},
|
||||
%{
|
||||
handle: "jehrhardt",
|
||||
name: "Jan Ehrhardt",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/59441?v=4",
|
||||
channel_tagline: "Building Cozy Auth - Elixir Rewrite and going wild on Claude AI 🚀"
|
||||
},
|
||||
%{
|
||||
handle: "zachdaniel",
|
||||
name: "Zach Daniel",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/5722339?v=4",
|
||||
channel_tagline: "Writing rad Elixir"
|
||||
},
|
||||
%{
|
||||
handle: "midday",
|
||||
name: "Midday",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/655158?v=4",
|
||||
channel_tagline: "Midday Product Hunt Launch"
|
||||
}
|
||||
]
|
||||
|> Enum.each(fn user_data ->
|
||||
case Accounts.get_user_by(handle: user_data.handle) do
|
||||
nil ->
|
||||
Repo.insert!(%User{
|
||||
handle: user_data.handle,
|
||||
name: user_data.name,
|
||||
avatar_url: user_data.avatar_url,
|
||||
email: "#{user_data.handle}@example.com",
|
||||
visibility: :public,
|
||||
is_live: false,
|
||||
channel_tagline: user_data.channel_tagline
|
||||
})
|
||||
|
||||
existing_user ->
|
||||
existing_user
|
||||
end
|
||||
end)
|
||||
|
||||
[
|
||||
%{
|
||||
title: "Build in public",
|
||||
slug: "buildinpublic",
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/7/cover/1717089683"
|
||||
},
|
||||
%{
|
||||
title: "Solving bounties live",
|
||||
slug: "bounties",
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/5/cover/1717077107"
|
||||
},
|
||||
%{
|
||||
title: "COSS Founder Podcast",
|
||||
slug: "coss-founder-podcast",
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/4/cover/1717076436"
|
||||
},
|
||||
%{
|
||||
title: "eu/acc - Update :)",
|
||||
slug: "eu-acc",
|
||||
scheduled_for: ~N[2024-05-31 16:00:00],
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/2/cover/1716648718"
|
||||
},
|
||||
%{
|
||||
title: "The Save File",
|
||||
slug: "the_savefile",
|
||||
scheduled_for: ~N[2024-06-21 17:30:00],
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/8/cover/1717155673"
|
||||
},
|
||||
%{
|
||||
title: "RFC 007 - Demos!",
|
||||
slug: "rfc",
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/1/cover/1716648933"
|
||||
},
|
||||
%{
|
||||
title: "COSS Office Hours",
|
||||
slug: "coss",
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/3/cover/1716657591"
|
||||
},
|
||||
%{
|
||||
title: "The TSPerf Challenge",
|
||||
slug: "tsperf",
|
||||
image_url: "https://fly.storage.tigris.dev/algora/shows/6/cover/1717861791"
|
||||
}
|
||||
]
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.each(fn {show_data, index} ->
|
||||
case Repo.get_by(Show, slug: show_data.slug) do
|
||||
nil ->
|
||||
Repo.insert!(%Show{
|
||||
user_id: user.id,
|
||||
title: show_data.title,
|
||||
slug: show_data.slug,
|
||||
scheduled_for: Map.get(show_data, :scheduled_for),
|
||||
image_url: show_data.image_url,
|
||||
ordering: index
|
||||
})
|
||||
|
||||
existing_show ->
|
||||
existing_show
|
||||
end
|
||||
end)
|
||||
|
BIN
priv/static/images/shows/build-in-public.jpg
Normal file
After Width: | Height: | Size: 512 KiB |
BIN
priv/static/images/shows/coding-challenges.jpg
Normal file
After Width: | Height: | Size: 359 KiB |
BIN
priv/static/images/shows/coss-founder-podcast.jpg
Normal file
After Width: | Height: | Size: 537 KiB |
BIN
priv/static/images/shows/coss-office-hours.jpg
Normal file
After Width: | Height: | Size: 447 KiB |
BIN
priv/static/images/shows/eu-acc.jpg
Normal file
After Width: | Height: | Size: 166 KiB |
BIN
priv/static/images/shows/live-bounty-hunting.jpg
Normal file
After Width: | Height: | Size: 504 KiB |
BIN
priv/static/images/shows/request-for-comments.jpg
Normal file
After Width: | Height: | Size: 475 KiB |
BIN
priv/static/images/shows/the-save-file.jpg
Normal file
After Width: | Height: | Size: 563 KiB |