Add users

This commit is contained in:
Mirai Kumiko 2025-06-02 17:30:35 +02:00
parent 956f2625fd
commit 162aa095d3
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
15 changed files with 489 additions and 99 deletions

View file

@ -1,64 +1,21 @@
defmodule NullaWeb.UserController do
use NullaWeb, :controller
alias Nulla.Users
alias Nulla.InstanceSettings
alias Nulla.ActivityPub
def show(conn, %{"username" => username}) do
accept = get_req_header(conn, "accept") |> List.first() || ""
accept = List.first(get_req_header(conn, "accept"))
instance_settings = InstanceSettings.get_instance_settings!()
domain = instance_settings.domain
user = Users.get_user_by_username!(username)
if String.contains?(accept, "application/activity+json") or String.contains?(accept, "application/ld+json") do
if accept in ["application/activity+json", "application/ld+json"] do
conn
|> put_resp_content_type("application/activity+json")
|> json(%{
id: "https://localhost/@#{username}",
type: "Person",
following: "https://localhost/@#{username}/following",
followers: "https://localhost/@#{username}/followers",
inbox: "https://localhost/@#{username}/inbox",
outbox: "https://localhost/@#{username}/outbox",
featured: "https://localhost/@#{username}/collections/featured",
preferredUsername: "miraikumiko",
name: "Mirai Kumiko",
summary: "Lol Kek Cheburek",
url: "https://localhost/@#{username}",
manuallyApprovesFollowers: false,
discoverable: true,
indexable: true,
published: "2025-05-05T00:00:00Z",
memorial: false,
publicKey: %{
id: "https://localhost/@#{username}#main-key",
owner: "https://localhost/@#{username}",
publicKeyPem: "public key"
},
tag: [
%{
type: "Hashtag",
href: "https://localhost/tags/linux",
name: "#linux"
}
],
attachment: [
%{
type: "PropertyValue",
name: "Website",
value: "<a href=\"https://miraikumiko.com/\" rel=\"me nofollow noopener\" target=\"_blank\">https://miraikumiko.com</a>"
}
],
endpoints: %{
sharedInbox: "https://localhost/inbox"
},
icon: %{
type: "Image",
mediaType: "image/jpeg",
url: "url"
},
image: %{
type: "Image",
mediaType: "image/jpeg",
url: "url"
}
})
|> json(ActivityPub.ap_user(domain, user))
else
render(conn, :show, username: username, layout: false)
render(conn, :show, user: user, domain: domain, layout: false)
end
end
end

View file

@ -0,0 +1,62 @@
defmodule NullaWeb.UserController do
use NullaWeb, :controller
alias Nulla.Users
alias Nulla.Users.User
def index(conn, _params) do
users = Users.list_users()
render(conn, :index, users: users)
end
def new(conn, _params) do
changeset = Users.change_user(%User{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Users.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: ~p"/users/#{user}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
user = Users.get_user!(id)
render(conn, :show, user: user)
end
def edit(conn, %{"id" => id}) do
user = Users.get_user!(id)
changeset = Users.change_user(user)
render(conn, :edit, user: user, changeset: changeset)
end
def update(conn, %{"id" => id, "user" => user_params}) do
user = Users.get_user!(id)
case Users.update_user(user, user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User updated successfully.")
|> redirect(to: ~p"/users/#{user}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, user: user, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
user = Users.get_user!(id)
{:ok, _user} = Users.delete_user(user)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: ~p"/users")
end
end

View file

@ -1,10 +1,41 @@
defmodule NullaWeb.UserHTML do
@moduledoc """
This module contains pages rendered by UserController.
See the `user_html` directory for all templates available.
"""
use NullaWeb, :html
embed_templates "user_html/*"
@doc """
Renders a user form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def user_form(assigns)
def format_birthdate(date) do
formatted = Date.to_string(date) |> String.replace("-", "/")
age = Timex.diff(Timex.today(), date, :years)
"#{formatted} (#{age} years old)"
end
def format_registration_date(date) do
now = Timex.now()
formatted = Date.to_string(date) |> String.replace("-", "/")
diff = Timex.diff(now, date, :days)
relative =
cond do
diff == 0 -> "today"
diff == 1 -> "1 day ago"
diff < 30 -> "#{diff} days ago"
diff < 365 ->
months = Timex.diff(now, date, :months)
if months == 1, do: "1 month ago", else: "#{months} months ago"
true ->
years = Timex.diff(now, date, :years)
if years == 1, do: "1 year ago", else: "#{years} years ago"
end
"#{formatted} (#{relative})"
end
end

View file

@ -0,0 +1,8 @@
<.header>
Edit User {@user.id}
<:subtitle>Use this form to manage user records in your database.</:subtitle>
</.header>
<.user_form changeset={@changeset} action={~p"/users/#{@user}"} />
<.back navigate={~p"/users"}>Back to users</.back>

View file

@ -0,0 +1,23 @@
<.header>
Listing Users
<:actions>
<.link href={~p"/users/new"}>
<.button>New User</.button>
</.link>
</:actions>
</.header>
<.table id="users" rows={@users} row_click={&JS.navigate(~p"/users/#{&1}")}>
<:col :let={user} label="Username">{user.username}</:col>
<:action :let={user}>
<div class="sr-only">
<.link navigate={~p"/users/#{user}"}>Show</.link>
</div>
<.link navigate={~p"/users/#{user}/edit"}>Edit</.link>
</:action>
<:action :let={user}>
<.link href={~p"/users/#{user}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

View file

@ -0,0 +1,8 @@
<.header>
New User
<:subtitle>Use this form to manage user records in your database.</:subtitle>
</.header>
<.user_form changeset={@changeset} action={~p"/users"} />
<.back navigate={~p"/users"}>Back to users</.back>

View file

@ -14,50 +14,56 @@
<img src={~p"/images/avatar.jpg"} class="absolute left-4 bottom-0 translate-y-1/2 rounded-full border-4 border-white w-[8.33vw] h-[8.33vw] min-w-[80px] min-h-[80px] max-w-[160px] max-h-[160px]"/>
</div>
<div class="mt-[4.5vw] px-4 flex flex-col">
<span class="text-xl font-bold">Mirai Kumiko</span>
<span class="text-gray-500">@miraikumiko@nulla.social</span>
<span class="text-xl font-bold">{@user.realname}</span>
<span class="text-gray-500">@{@user.username}@{@domain}</span>
<div class="text-sm pt-2">
<p>Cryptopunk in the past.</p>
<p>Silent girl now and admin of this instance.</p>
<br>
<p>Grew up on hacker culture, philosophy, good old movies and anime. That's why I love cyberpunk — modern philosophy and technolization in one bottle. I also use Linux on a first-name basis and can program.</p>
<br>
<p>Can play shooters, chess and other games where strategy and psychological analysis of opponents are important.</p>
<br>
<p>Bunnies and rabbits are superior!</p>
<p>{@user.bio}</p>
</div>
<dl class="mt-2 space-y-1 text-sm text-gray-700">
<div class="flex items-center gap-2">
<dt><.icon name="hero-map-pin" class="mt-0.5 h-5 w-5 flex-none" /></dt>
<dd>Catalonia, Spain</dd>
</div>
<div class="flex items-center gap-2">
<dt><.icon name="hero-cake" class="mt-0.5 h-5 w-5 flex-none" /></dt>
<dd>2005/02/25 (20 years old)</dd>
</div>
<div class="flex items-center gap-2">
<dt><.icon name="hero-calendar" class="mt-0.5 h-5 w-5 flex-none" /></dt>
<dd>03/20/2025 (2mo ago)</dd>
</div>
</dl>
<dl class="pt-5">
<div class="flex items-center gap-5">
<dt>Website</dt>
<dd>
<a href="https://miraikumiko.com" class="text-[#1D9BF0]">miraikumiko.com</a>
</dd>
</div>
<dl class="mt-2 text-sm text-gray-700 grid grid-cols-[auto,1fr] gap-x-2 gap-y-1 items-center">
<%= if @user.location do %>
<dt class="flex items-center gap-2">
<.icon name="hero-map-pin" class="mt-0.5 h-5 w-5 flex-none" />
</dt>
<dd><%= @user.location %></dd>
<% end %>
<%= if @user.birthday do %>
<dt class="flex items-center gap-2">
<.icon name="hero-cake" class="mt-0.5 h-5 w-5 flex-none" />
</dt>
<dd><%= format_birthdate(@user.birthday) %></dd>
<% end %>
<dt class="flex items-center gap-2">
<.icon name="hero-calendar" class="mt-0.5 h-5 w-5 flex-none" />
</dt>
<dd><%= format_registration_date(@user.inserted_at) %></dd>
</dl>
<%= if @user.fields do %>
<dl class="mt-5 grid grid-cols-[max-content,1fr] gap-x-5 gap-y-2 items-center">
<%= for {key, value} <- @user.fields do %>
<dt><%= key %></dt>
<dd>
<%= if Regex.match?(~r{://}, value) do %>
<a href={value} class="text-[#1D9BF0]"><%= Regex.replace(~r{^\w+://}, value, "") %></a>
<% else %>
<%= value %>
<% end %>
</dd>
<% end %>
</dl>
<% end %>
<div class="flex mt-5 gap-3">
<a href="/@miraikumiko">1.7K <span class="text-gray-700">Posts</span></a>
<a href="/@miraikumiko/following">33 <span class="text-gray-700">Following</span></a>
<a href="/@miraikumiko/followers">31 <span class="text-gray-700">Followers</span></a>
<a href={~p"/@#{@user.username}"}>1.7K Posts</a>
<a href={~p"/@#{@user.username}/following"}>33 Following</a>
<a href={~p"/@#{@user.username}/followers"}>31 Followers</a>
</div>
</div>
<div class="flex justify-between px-20 py-2 mt-5 border border-gray-300">
<div>Posts</div>
<div>Posts and replies</div>
<div>Media</div>
<a href={~p"/@#{@user.username}/featured"}>Featured</a>
<a href={~p"/@#{@user.username}"}>Posts</a>
<a href={~p"/@#{@user.username}/with_replies"}>Posts and replies</a>
<a href={~p"/@#{@user.username}/media"}>Media</a>
</div>
</div>
<div class="flex flex-col items-center mt-5 gap-5">

View file

@ -0,0 +1,15 @@
<.header>
User {@user.id}
<:subtitle>This is a user record from your database.</:subtitle>
<:actions>
<.link href={~p"/users/#{@user}/edit"}>
<.button>Edit user</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Username">{@user.username}</:item>
</.list>
<.back navigate={~p"/users"}>Back to users</.back>

View file

@ -0,0 +1,9 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:username]} type="text" label="Username" />
<:actions>
<.button>Save User</.button>
</:actions>
</.simple_form>

View file

@ -19,6 +19,7 @@ defmodule NullaWeb.Router do
get "/", PageController, :home
get "/@:username", UserController, :show
resources "/users", UserController
end
# Other scopes may use custom stacks.