Packages
phoenix_kit
1.7.160
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/shared/components/video.ex
defmodule PhoenixKit.Modules.Shared.Components.Video do
@moduledoc """
Responsive YouTube embed component.
Supports both explicit `video_id` and full YouTube URLs.
Optional attributes:
* `autoplay` - "true" | "false" (default)
* `muted` - "true" | "false" (default)
* `controls` - "true" | "false" (default)
* `loop` - "true" | "false" (default)
* `start` - start time in seconds
* `ratio` - aspect ratio string (e.g., "16:9", "4:3", "1:1")
Component body text is rendered as a caption beneath the player.
"""
use Phoenix.Component
alias PhoenixKit.Utils.Values
@standard_youtube_hosts [
"youtube.com",
"www.youtube.com",
"m.youtube.com",
"music.youtube.com"
]
@short_youtube_hosts [
"youtu.be"
]
attr(:attributes, :map, default: %{})
attr(:variant, :string, default: "default")
attr(:content, :string, default: nil)
def render(assigns) do
attrs = assigns.attributes || %{}
video_id =
attrs
|> Map.get("video_id")
|> Values.presence() || extract_video_id(Map.get(attrs, "url"))
assigns =
assigns
|> assign(:video_id, video_id)
|> assign(:embed_url, build_embed_url(video_id, attrs))
|> assign(:aspect_ratio_class, ratio_class(Map.get(attrs, "ratio", "16:9")))
|> assign(:has_caption?, has_caption?(assigns.content))
|> assign(:caption, assigns.content)
~H"""
<%= if @video_id do %>
<div class="phk-video space-y-4">
<div class={[
"relative w-full overflow-hidden rounded-2xl shadow-xl bg-base-200",
@aspect_ratio_class
]}>
<iframe
src={@embed_url}
class="absolute inset-0 h-full w-full"
title="YouTube video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
loading="lazy"
></iframe>
</div>
<%= if @has_caption? do %>
<p class="text-sm text-base-content/70 text-center">
{@caption}
</p>
<% end %>
</div>
<% else %>
<div class="phk-video phk-video--invalid w-full rounded-xl border border-dashed border-error/40 bg-error/5 p-6 text-center text-sm text-error">
Unable to load video – please supply a valid YouTube `video_id` or `url`.
</div>
<% end %>
"""
end
defp build_embed_url(nil, _attrs), do: nil
defp build_embed_url(video_id, attrs) do
base = "https://www.youtube.com/embed/#{video_id}"
params =
[
{"autoplay", truthy?(Map.get(attrs, "autoplay"))},
{"mute", truthy?(Map.get(attrs, "muted"))},
{"controls", controls_value(Map.get(attrs, "controls"))},
{"loop", truthy?(Map.get(attrs, "loop"))},
{"start", parse_integer(Map.get(attrs, "start"))}
]
|> Enum.reduce([], fn
{"loop", true}, acc -> [{"loop", "1"}, {"playlist", video_id} | acc]
{"loop", _}, acc -> acc
{"autoplay", true}, acc -> [{"autoplay", "1"} | acc]
{"autoplay", _}, acc -> acc
{"mute", true}, acc -> [{"mute", "1"} | acc]
{"mute", _}, acc -> acc
{"controls", value}, acc when value in ["0", "1"] -> [{"controls", value} | acc]
{"start", nil}, acc -> acc
{"start", value}, acc -> [{"start", Integer.to_string(value)} | acc]
_, acc -> acc
end)
|> Enum.reverse()
if params == [] do
base
else
base <> "?" <> URI.encode_query(params)
end
end
defp ratio_class(ratio) do
case ratio do
"4:3" -> "aspect-[4/3]"
"1:1" -> "aspect-square"
"21:9" -> "aspect-[21/9]"
_ -> "aspect-video"
end
end
defp extract_video_id(nil), do: nil
defp extract_video_id(url) when is_binary(url) do
with trimmed when trimmed != "" <- String.trim(url),
{:ok, uri} <- parse_uri(trimmed),
{:ok, host_type} <- classify_host(uri.host) do
case host_type do
:short -> extract_short_id(uri.path)
:standard -> extract_standard_id(uri)
end
else
_ -> nil
end
end
defp extract_video_id(_), do: nil
defp truthy?(true), do: true
defp truthy?(false), do: false
defp truthy?(value) when is_binary(value) do
value = value |> String.trim() |> String.downcase()
value in ["true", "1", "yes", "on"]
end
defp truthy?(value) when is_integer(value), do: value != 0
defp truthy?(_), do: false
defp controls_value(value) do
if explicitly_false?(value), do: "0", else: "1"
end
defp explicitly_false?(value) when is_binary(value) do
value
|> String.trim()
|> String.downcase()
|> case do
"" -> false
v -> v in ["false", "0", "no", "off"]
end
end
defp explicitly_false?(false), do: true
defp explicitly_false?(0), do: true
defp explicitly_false?(_), do: false
defp parse_integer(nil), do: nil
defp parse_integer(value) when is_binary(value) do
value
|> String.trim()
|> case do
"" ->
nil
number ->
case Integer.parse(number) do
{int, _} when int >= 0 -> int
_ -> nil
end
end
end
defp parse_integer(value) when is_integer(value) and value >= 0, do: value
defp parse_integer(_), do: nil
defp has_caption?(value) when is_binary(value) do
String.trim(value) != ""
end
defp has_caption?(_), do: false
defp parse_uri(string) when is_binary(string) do
case URI.parse(string) do
%URI{scheme: scheme, host: host} = uri
when scheme in ["http", "https"] and is_binary(host) ->
{:ok, uri}
_ ->
:error
end
end
defp classify_host(host) when is_binary(host) do
host = String.downcase(host)
cond do
host in @short_youtube_hosts -> {:ok, :short}
host in @standard_youtube_hosts -> {:ok, :standard}
true -> :error
end
end
defp extract_short_id(nil), do: nil
defp extract_short_id(path) do
path
|> String.trim_leading("/")
|> String.split("/", trim: true)
|> List.first()
end
defp extract_standard_id(%URI{path: path} = uri) do
if is_binary(path) and String.starts_with?(path, "/embed/") do
path
|> String.split("/", trim: true)
|> List.last()
else
uri.query
|> decode_query()
|> Map.get("v")
end
end
defp decode_query(nil), do: %{}
defp decode_query(query) do
query
|> URI.query_decoder()
|> Enum.into(%{})
end
end