3.0 KiB · text 5af55d9
defmodule GitGudWeb.ProfileComponents do
@moduledoc """
Shared chrome for user + org profile pages.
Both `users` and `organizations` rows carry the same profile fields:
display_name, bio, url, location, company, public_email, timezone,
and a `socials` JSON map. The components in here render those fields
the same way regardless of the underlying schema.
"""
use Phoenix.Component
import GitGudWeb.CoreComponents, only: [icon: 1]
attr :profile, :map, required: true, doc: "User or Organization struct"
def details(assigns) do
socials = social_items(assigns.profile)
assigns = assign(assigns, :socials, socials)
~H"""
<dl class="text-sm space-y-1.5">
<.row :if={@profile.bio} class="opacity-90">
<span class="whitespace-pre-wrap">{@profile.bio}</span>
</.row>
<.row :if={@profile.company} icon="hero-building-office">
{@profile.company}
</.row>
<.row :if={@profile.location} icon="hero-map-pin">
{@profile.location}
</.row>
<.row :if={@profile.url} icon="hero-link">
<a href={@profile.url} class="link link-hover" rel="noreferrer noopener">
{@profile.url}
</a>
</.row>
<.row :if={@profile.public_email} icon="hero-envelope">
<a href={"mailto:" <> @profile.public_email} class="link link-hover">
{@profile.public_email}
</a>
</.row>
<.row :if={@profile.timezone} icon="hero-clock">
{@profile.timezone}
</.row>
<.row :if={@socials != []} icon="hero-globe-alt">
<ul class="flex flex-wrap gap-x-3 gap-y-1">
<li :for={s <- @socials}>
<a href={s.url} class="link link-hover" rel="noreferrer noopener">
{s.platform}<%= if s.handle != "", do: ": " <> s.handle %>
</a>
</li>
</ul>
</.row>
</dl>
"""
end
attr :icon, :string, default: nil
attr :class, :string, default: ""
slot :inner_block, required: true
defp row(assigns) do
~H"""
<div class={["flex items-baseline gap-2", @class]}>
<.icon :if={@icon} name={@icon} class="size-4 opacity-60 shrink-0 self-center" />
<dd class="min-w-0 break-words">{render_slot(@inner_block)}</dd>
</div>
"""
end
@doc """
Normalize the `socials` JSONB blob into a list of `%{platform, handle,
url}` maps. Tolerant of empty/missing/legacy shapes.
"""
def social_items(%{socials: %{"items" => items}}) when is_list(items) do
items
|> Enum.map(&normalize_social/1)
|> Enum.reject(&is_nil/1)
end
def social_items(_), do: []
defp normalize_social(%{} = m) do
platform = trim_or_nil(m["platform"])
url = trim_or_nil(m["url"])
handle = m["handle"] |> to_string() |> String.trim()
cond do
is_binary(platform) and is_binary(url) ->
%{platform: platform, handle: handle, url: url}
true ->
nil
end
end
defp normalize_social(_), do: nil
defp trim_or_nil(s) when is_binary(s) do
case String.trim(s) do
"" -> nil
x -> x
end
end
defp trim_or_nil(_), do: nil
end