Added more view functions for HTML page renders, including one's for user authentication
This commit is contained in:
@@ -13,12 +13,42 @@ defmodule SukaatoWeb.ErrorHTML do
|
||||
# * lib/sukaato_web/controllers/error_html/404.html.heex
|
||||
# * lib/sukaato_web/controllers/error_html/500.html.heex
|
||||
#
|
||||
# embed_templates "error_html/*"
|
||||
embed_templates "error_html/*"
|
||||
|
||||
# The default is to render a plain text page based on
|
||||
# the template name. For example, "404.html" becomes
|
||||
# "Not Found".
|
||||
def render(template, _assigns) do
|
||||
Phoenix.Controller.status_message_from_template(template)
|
||||
# def render(template, _assigns) do
|
||||
# Phoenix.Controller.status_message_from_template(template)
|
||||
# end
|
||||
|
||||
@site_config_file Path.expand("../../../site.toml", __DIR__)
|
||||
@site_config elem(Toml.decode_file(@site_config_file), 1)
|
||||
|
||||
attr :site_name, :string, default: @site_config["site"]["name"]
|
||||
attr :site_author, :string, default: @site_config["site"]["author"]
|
||||
attr :site_desc, :string, default: @site_config["site"]["desc"]
|
||||
|
||||
def html_head(assigns) do
|
||||
~H"""
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<% # <meta name="keywords" content={@site_keywords}> %>
|
||||
<meta name="author" content={@site_author}>
|
||||
<meta name="description" content={@site_desc}>
|
||||
<link rel="stylesheet" href={~p"/assets/app.css"}>
|
||||
<title><%= @site_name %></title>
|
||||
</head>
|
||||
"""
|
||||
end
|
||||
|
||||
def html_foot(assigns) do
|
||||
# @TODO do HEEx loop on @badge_collection
|
||||
~H"""
|
||||
<footer>
|
||||
<% # @TODO add list of badges here or an anchor element with attribute set to a badges route %>
|
||||
</footer>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
@@ -1,9 +1,239 @@
|
||||
defmodule SukaatoWeb.PageController do
|
||||
alias SukaatoWeb.Theme
|
||||
alias Sukaato.{Auth, StandardQueries, User}
|
||||
alias QRCode.Render
|
||||
import Ecto.Query
|
||||
use SukaatoWeb, :controller
|
||||
|
||||
def home(conn, _params) do
|
||||
# The home page is often custom made,
|
||||
# so skip the default app layout.
|
||||
render(conn, :home, layout: false)
|
||||
@rel_proj_root "../../.."
|
||||
@site_config_file Path.expand(@rel_proj_root <> "/site.toml", __DIR__)
|
||||
@site_config Toml.decode_file(@site_config_file)
|
||||
@admin_username elem(@site_config, 1)["site"]["username"]
|
||||
|
||||
@page_links [
|
||||
%PageLink{name: "Infernus", uri: "/"},
|
||||
%PageLink{name: "Talisman", uri: "/pubkey"},
|
||||
%PageLink{name: "Evokation", uri: "/contact"},
|
||||
%PageLink{name: "Prophets", uri: "/thinkers"}
|
||||
# @TODO place link as anchor element href attribute value inside template element for ALL pages
|
||||
# %PageLink{name: "Passage", uri: "/login"}
|
||||
]
|
||||
|
||||
@tables %{
|
||||
user: User
|
||||
}
|
||||
|
||||
def index(conn, _params) do
|
||||
Theme.update(elem(@site_config, 1)["site"]["theme"])
|
||||
render(conn, :index, layout: false, config: elem(@site_config, 1), page_name: "index", pages: @page_links)
|
||||
end
|
||||
|
||||
def pubkey(conn, params) do
|
||||
Theme.update(elem(@site_config, 1)["site"]["theme"])
|
||||
user = if Map.has_key?(params, "user"), do: params["user"], else: @admin_username
|
||||
id = if Map.has_key?(params, "id"), do: params["id"], else: ""
|
||||
result = StandardQueries.get_pubkeys(user, id)
|
||||
data = if tuple_size(result) >= 2, do: elem(result, 1), else: []
|
||||
gpg_id = if length(data) > 0, do: Enum.map(data, fn d -> elem(d, 0) end), else: [""]
|
||||
content = if length(data) > 0, do: Enum.map(data, fn d -> elem(d, 1) end), else: [""]
|
||||
qr_embed = {Path.expand("../../../priv/static/images/sigil.svg", __DIR__), 200}
|
||||
qr_settings = %Render.SvgSettings{image: qr_embed, structure: :readable, background_opacity: 0.0, scale: 4}
|
||||
|
||||
# @TODO account for case wherein pubkey_content is a list
|
||||
if length(content) == 1 do
|
||||
qr = if Enum.at(content, 0) != "", do: QRCode.create(Enum.at(content, 0)), else: ""
|
||||
qr = QRCode.render(qr, :svg, qr_settings)
|
||||
id_qr = Enum.zip([gpg_id, [qr], content])
|
||||
|
||||
render(conn, :pubkey, layout: false, config: elem(@site_config, 1), pages: @page_links, user: user, qr: id_qr)
|
||||
else
|
||||
# content = if length(content) > 0, do: Enum.filter(content, fn c -> c != "" end), else: []
|
||||
qr = if length(content) > 1, do: Enum.map(content, fn c -> QRCode.create(c) end), else: [""]
|
||||
qr = Enum.map(qr, fn q -> QRCode.render(q, :svg, qr_settings) end)
|
||||
id_qr = Enum.zip([gpg_id, qr, content])
|
||||
id_qr = Enum.filter(id_qr, fn q -> elem(q, 1) != "" end)
|
||||
|
||||
render(conn, :pubkey, layout: false, config: elem(@site_config, 1), pages: @page_links, user: user, qr: id_qr)
|
||||
end
|
||||
end
|
||||
|
||||
def think(conn, params) do
|
||||
Theme.update(elem(@site_config, 1)["site"]["theme"])
|
||||
username = if Map.has_key?(params, "user"), do: params["user"], else: @admin_username
|
||||
thinker_data = File.read(Path.expand(@rel_proj_root <> "/priv/static/files/users/" <> username <> "/thinkers.json", __DIR__))
|
||||
thinker_data = if elem(thinker_data, 0) == :ok, do: elem(thinker_data, 1)
|
||||
thinker_data = elem(Jason.decode(thinker_data, keys: :atoms), 1)
|
||||
|
||||
render(conn, :thinkers, layout: false, config: elem(@site_config, 1), pages: @page_links, page_name: "thinkers", thinkers: thinker_data)
|
||||
end
|
||||
|
||||
def contact(conn, params) do
|
||||
Theme.update(elem(@site_config, 1)["site"]["theme"])
|
||||
affil = if Map.has_key?(params, "user"), do: StandardQueries.get_inboxes(params["user"]), else: StandardQueries.get_inboxes(@admin_username)
|
||||
affil = elem(affil, 1)
|
||||
|
||||
render(conn, :contact, layout: false, config: elem(@site_config, 1), pages: @page_links, affil: affil)
|
||||
end
|
||||
|
||||
def two_fa(conn, _params) do
|
||||
pass_token = get_session(conn, "pass_token")
|
||||
twofa_token = get_session(conn, "2fa_token")
|
||||
# IO.inspect(conn)
|
||||
|
||||
if is_nil(pass_token) do
|
||||
redirect(conn, to: ~p"/login")
|
||||
# status = put_status(conn, 404)
|
||||
# ext = ".heex"
|
||||
# render(status, ErrorView, "404.html" <> ext)
|
||||
else
|
||||
user_id = Enum.at(String.split(pass_token, ":", parts: 2), 0)
|
||||
|
||||
if is_nil(twofa_token) do
|
||||
twofact = Auth.get_credentials(conn, ["code", "chall_resp"])
|
||||
query = from u in @tables.user, select: {u.totp_active, u.totp_enabled}, where: u.username == ^user_id or u.email == ^user_id
|
||||
totp_requisite = Auth.is_authflow_required?(user_id, :all?, (query))
|
||||
query = from u in @tables.user, select: {u.fido_active, u.fido_enabled}, where: u.username == ^user_id or u.email == ^user_id
|
||||
fido_requisite = Auth.is_authflow_required?(user_id, :all?, (query))
|
||||
|
||||
if is_nil(twofact) do
|
||||
render(conn, :twofact, layout: false, config: elem(@site_config, 1), pages: @page_links, attempted: false, totp: totp_requisite, fido: fido_requisite)
|
||||
else
|
||||
result = if Map.has_key?(twofact, "code"), do: Auth.totp_bouncer(conn, user_id, twofact["code"]), else: {conn, !totp_requisite, nil}
|
||||
conn = elem(result, 0)
|
||||
authenticated = elem(result, 1)
|
||||
IO.inspect(authenticated)
|
||||
result = if Map.has_key?(twofact, "chall_resp"), do: Auth.fido_bouncer(conn, user_id, twofact["chall_resp"]), else: {conn, !fido_requisite, nil}
|
||||
conn = elem(result, 0)
|
||||
IO.inspect(elem(result, 1))
|
||||
authenticated = authenticated and elem(result, 1)
|
||||
IO.inspect(authenticated)
|
||||
|
||||
if authenticated do
|
||||
# conn = elem(result, 0)
|
||||
conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "2fa_token")
|
||||
conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "pass_token")
|
||||
conn = Auth.generate_user_session(conn, user_id, true, "login_token")
|
||||
redirect(conn, to: ~p"/login")
|
||||
else
|
||||
render(conn, :twofact, layout: false, config: elem(@site_config, 1), pages: @page_links, attempted: true, totp: totp_requisite, fido: fido_requisite)
|
||||
end
|
||||
end
|
||||
else
|
||||
conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "2fa_token")
|
||||
conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "pass_token")
|
||||
conn = Auth.generate_user_session(conn, user_id, true, "login_token")
|
||||
redirect(conn, to: ~p"/login")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def login(conn, _params) do
|
||||
Theme.update(elem(@site_config, 1)["site"]["theme"])
|
||||
|
||||
demand = Auth.get_credentials(conn, ["logout"])
|
||||
want_logout = if is_nil(demand), do: "false", else: demand["logout"]
|
||||
current_login_token = get_session(conn, "login_token")
|
||||
result = if want_logout == "true" and !is_nil(current_login_token), do: StandardQueries.dump_login_token(current_login_token), else: {:ok, %{}}
|
||||
conn = if want_logout == "true" and elem(result, 0) == :ok, do: delete_session(conn, "login_token"), else: conn
|
||||
|
||||
pass_token = get_session(conn, "pass_token")
|
||||
twofa_token = get_session(conn, "2fa_token")
|
||||
login_token = get_session(conn, "login_token")
|
||||
|
||||
submission_names = [
|
||||
"Act as Clergy", # @NOTE logging in
|
||||
"Return to Laity" # @NOTE logging out
|
||||
]
|
||||
button_name_options = %{
|
||||
names: submission_names
|
||||
}
|
||||
form_fields = %{
|
||||
username: "Clerical Title",
|
||||
password: "Incantation",
|
||||
code: "Sacred Number"
|
||||
}
|
||||
|
||||
if is_nil(login_token) do
|
||||
if is_nil(pass_token) do
|
||||
# PSEUDOCODE
|
||||
# - Create empty map
|
||||
# - Attempt to acquire username from conn's body parameters
|
||||
# - Attempt to place non-nil username in key added to empty map
|
||||
# - Attempt to acquire password from conn's body parameters
|
||||
# - Attempt to place non-nil password in key added to empty map
|
||||
# - If these keys exist in map or their values are not nil, call password authentication function
|
||||
# -- When password authentication succeeds
|
||||
# --- Place non-nil value in session key pass_token
|
||||
# --- When 2fa step is necessary (can be left to redirect route)
|
||||
# ---- Redirect to 2fa route and associated controller
|
||||
# --- Otherwise when 2fa step is unnecessary (can be left to redirect route)
|
||||
# ---- Place non-nil value in session key login_token
|
||||
# ---- Redirect to this controller's route
|
||||
# -- When password authentication fails, redirect to this controller's router
|
||||
|
||||
user = Auth.get_credentials(conn, ["username", "password"])
|
||||
result = if is_nil(user), do: nil, else: Auth.password_bouncer(conn, user["username"], user["password"])
|
||||
|
||||
if is_nil(result) do
|
||||
button_choice = Map.put(button_name_options, :choice, 0)
|
||||
render(conn, :login, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice, attempted: false)
|
||||
else
|
||||
authenticated = elem(result, 1)
|
||||
|
||||
if authenticated do
|
||||
conn = elem(result, 0)
|
||||
redirect(conn, to: ~p"/login") # -> should result in either logout or two-factor page
|
||||
else
|
||||
button_choice = Map.put(button_name_options, :choice, 0)
|
||||
render(conn, :login, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice, attempted: true)
|
||||
end
|
||||
end
|
||||
else
|
||||
if is_nil(twofa_token) do
|
||||
# PSEUDOCODE
|
||||
# - When 2fa step is necessary (unless left to 2fa route)
|
||||
# -- Redirect to 2fa route and associated controller
|
||||
# - Otherwise when 2fa step is unnecessary (unless left to 2fa route)
|
||||
# -- Place non-nil value in session key login_token
|
||||
# -- Redirect to this controller's router
|
||||
|
||||
user_id = Enum.at(String.split(pass_token, ":", parts: 2), 0)
|
||||
twofact_required = Auth.is_authflow_required?(user_id, :any?)
|
||||
# IO.inspect(twofact_required)
|
||||
|
||||
if twofact_required do
|
||||
redirect(conn, to: ~p"/second_factor")
|
||||
else
|
||||
conn = Auth.generate_user_session(conn, user_id, true, "login_token")
|
||||
conn = delete_session(conn, "pass_token")
|
||||
# redirect(conn, to: ~p"/login") # -> should result in logout page
|
||||
login_token = get_session(conn, "login_token")
|
||||
StandardQueries.absorb_login_token(login_token)
|
||||
button_choice = Map.put(button_name_options, :choice, 1)
|
||||
render(conn, :logout, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice)
|
||||
end
|
||||
else
|
||||
# PSEUDOCODE
|
||||
# - Remove non-nil value in session key pass_token
|
||||
# - Place non-nil value in session key login_token
|
||||
# - Redirect to this controller's router
|
||||
|
||||
conn = delete_session(conn, "2fa_token")
|
||||
conn = delete_session(conn, "pass_token")
|
||||
user_id = Enum.at(String.split(pass_token, ":", parts: 2), 0)
|
||||
conn = Auth.generate_user_session(conn, user_id, true, "login_token")
|
||||
# redirect(conn, to: ~p"/login") # -> should result in logout page
|
||||
StandardQueries.absorb_login_token(login_token)
|
||||
button_choice = Map.put(button_name_options, :choice, 1)
|
||||
render(conn, :logout, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice)
|
||||
end
|
||||
end
|
||||
else
|
||||
conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "2fa_token")
|
||||
conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "pass_token")
|
||||
StandardQueries.absorb_login_token(login_token)
|
||||
button_choice = Map.put(button_name_options, :choice, 1)
|
||||
render(conn, :logout, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@@ -4,7 +4,141 @@ defmodule SukaatoWeb.PageHTML do
|
||||
|
||||
See the `page_html` directory for all templates available.
|
||||
"""
|
||||
alias SukaatoWeb.Marker
|
||||
alias SukaatoWeb.Theme
|
||||
use SukaatoWeb, :html
|
||||
|
||||
embed_templates "page_html/*"
|
||||
|
||||
@rel_proj_root "../../.."
|
||||
@site_config_file Path.expand(@rel_proj_root <> "/site.toml", __DIR__)
|
||||
@site_config elem(Toml.decode_file(@site_config_file), 1)
|
||||
|
||||
attr :audience, :string, default: "humans"
|
||||
attr :home, :string, default: "but one recessed sulci of the global encephalon"
|
||||
attr :site_name, :string, default: @site_config["site"]["name"]
|
||||
attr :site_author, :string, default: @site_config["site"]["author"]
|
||||
attr :site_desc, :string, default: @site_config["site"]["desc"]
|
||||
attr :badge_collection, :list, default: []
|
||||
attr :page_links, :list, default: [
|
||||
%PageLink{name: "Home", uri: "/"},
|
||||
%PageLink{name: "GPG", uri: "/pubkey"},
|
||||
%PageLink{name: "Contact", uri: "/contact"}
|
||||
]
|
||||
attr :page_query, :string, default: ""
|
||||
attr :thinkers, :list, default: [
|
||||
%Thinker{name: "Plato", uri: "https://3.bp.blogspot.com/-4tLynuYBkhQ/TqcL1B3lDVI/AAAAAAAAAB0/DqucJFRCgxo/s1600/Plato.jpg", type: ["Philosopher", "Social Theorist", "Political Theorist"], desc: "An important philosopher", concepts: ["theory of forms", "divisions of the soul"]}
|
||||
]
|
||||
attr :icon, :string, default: "default"
|
||||
attr :btn_name, :map, default: %{
|
||||
names: [
|
||||
"Log In",
|
||||
"Log Out",
|
||||
"Register"
|
||||
],
|
||||
choice: 0
|
||||
}
|
||||
|
||||
def greet(assigns) do
|
||||
~H"""
|
||||
<h1>Welcome, <%= @audience %>, to <%= @home %>!</h1>
|
||||
"""
|
||||
end
|
||||
|
||||
def html_head(assigns) do
|
||||
~H"""
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<% # <meta name="keywords" content={@site_keywords}> %>
|
||||
<meta name="author" content={@site_author}>
|
||||
<meta name="description" content={@site_desc}>
|
||||
<link rel="stylesheet" href={~p"/assets/app.css"}>
|
||||
<title><%= @site_name %></title>
|
||||
</head>
|
||||
"""
|
||||
end
|
||||
|
||||
def navify(assigns) do
|
||||
~H"""
|
||||
<%= if @page_links != nil do %>
|
||||
<%= if length(@page_links) > 0 do %>
|
||||
<%= for anchor <- @page_links do %>
|
||||
<a href={anchor.uri}><%= anchor.name %></a>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<a href={~p"/"}>Home</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
def submission(assigns) do
|
||||
~H"""
|
||||
<% button_name = Enum.at(@btn_name.names, @btn_name.choice) %>
|
||||
<input type="submit" class="acct_manager" value={button_name}>
|
||||
"""
|
||||
end
|
||||
|
||||
def markdown_content(assigns) do
|
||||
~H"""
|
||||
<% result = Marker.render_mark(@page_query) %>
|
||||
<% content = if elem(result, 0) == :ok, do: elem(result, 1) %>
|
||||
<%= raw content %>
|
||||
"""
|
||||
end
|
||||
|
||||
def thinkify(assigns) do
|
||||
~H"""
|
||||
<%= if @thinkers != nil do %>
|
||||
<%= if length(@thinkers) > 0 do %>
|
||||
<%= for thinker <- @thinkers do %>
|
||||
<!-- @TODO move the style attribute and width/height attribute values to respective SASS files -->
|
||||
<% underscored_name = String.split(thinker.name, " ") %>
|
||||
<% underscored_name = Enum.join(underscored_name, "_") %>
|
||||
<figure id={underscored_name} class="thinker">
|
||||
<span class="tname"><p><%= thinker.name %></p></span>
|
||||
<%= if Map.has_key?(thinker, :uri) do %>
|
||||
<img src={thinker.uri} /><br>
|
||||
<% end %>
|
||||
<figcaption>
|
||||
<%= if Map.has_key?(thinker, :type) do %>
|
||||
<% thinker_types = Enum.map(thinker.type, fn tt -> "<span>" <> tt <> "</span>" end) %>
|
||||
<% thinker_type = Enum.join(thinker_types, ", ") %>
|
||||
<span class="ttype"><%= raw thinker_type %></span><br>
|
||||
<% end %>
|
||||
<%= if Map.has_key?(thinker, :concepts) do %>
|
||||
<% thinker_concepts = Enum.map(thinker.concepts, fn tc -> "<span>" <> tc <> "</span>" end) %>
|
||||
<% thinker_concepts = Enum.join(thinker_concepts, ", ") %>
|
||||
<span class="concept_tags">concepts:<br><%= raw thinker_concepts %></span><br>
|
||||
<% end %>
|
||||
<%= if Map.has_key?(thinker, :desc) do %>
|
||||
<span class="tdesc"><%= thinker.desc %></span><br>
|
||||
<% end %>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
def affiliate(assigns) do
|
||||
~H"""
|
||||
<% current_theme = Theme.list(:current) %>
|
||||
<!-- @NOTE if using default theme and experiencing janky sizing, make sure to remove SVGwidth and height attributes in source image -->
|
||||
<!-- @NOTE if using default theme and SVG unresponsive to CSS color changes, make sure to remove fill and stroke SVG path attributes -->
|
||||
<% rel_proj_root = "../../.." %>
|
||||
<%= raw File.read!(Path.expand(rel_proj_root <> "/priv/static/images/themes/" <> current_theme <> "/icons/" <> @icon <> ".svg", __DIR__)) %>
|
||||
"""
|
||||
end
|
||||
|
||||
def html_foot(assigns) do
|
||||
# @TODO do HEEx loop on @badge_collection
|
||||
~H"""
|
||||
<footer>
|
||||
<% # @TODO add list of badges here or an anchor element with attribute set to a badges route %>
|
||||
</footer>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
@@ -1,222 +0,0 @@
|
||||
<.flash_group flash={@flash} />
|
||||
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
|
||||
<svg
|
||||
viewBox="0 0 1480 957"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 h-full w-full"
|
||||
preserveAspectRatio="xMinYMid slice"
|
||||
>
|
||||
<path fill="#EE7868" d="M0 0h1480v957H0z" />
|
||||
<path
|
||||
d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
|
||||
fill="#FF9F92"
|
||||
/>
|
||||
<path
|
||||
d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
|
||||
fill="#FA8372"
|
||||
/>
|
||||
<path
|
||||
d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
|
||||
fill="#E96856"
|
||||
fill-opacity=".6"
|
||||
/>
|
||||
<path
|
||||
d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
|
||||
fill="#C42652"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
|
||||
<div class="mx-auto max-w-xl lg:mx-0">
|
||||
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
|
||||
<path
|
||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
||||
fill="#FD4F00"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="text-brand mt-10 flex items-center text-sm font-semibold leading-6">
|
||||
Phoenix Framework
|
||||
<small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6">
|
||||
v{Application.spec(:phoenix, :vsn)}
|
||||
</small>
|
||||
</h1>
|
||||
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 text-balance">
|
||||
Peace of mind from prototype to production.
|
||||
</p>
|
||||
<p class="mt-4 text-base leading-7 text-zinc-600">
|
||||
Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
|
||||
</p>
|
||||
<div class="flex">
|
||||
<div class="w-full sm:w-auto">
|
||||
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
|
||||
<a
|
||||
href="https://hexdocs.pm/phoenix/overview.html"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
||||
<path d="m12 4 10-2v18l-10 2V4Z" fill="#18181B" fill-opacity=".15" />
|
||||
<path
|
||||
d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
|
||||
stroke="#18181B"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Guides & Docs
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phoenixframework/phoenix"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
|
||||
fill="#18181B"
|
||||
/>
|
||||
</svg>
|
||||
Source Code
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
d="M12 1v6M12 17v6"
|
||||
stroke="#18181B"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
fill="#18181B"
|
||||
fill-opacity=".15"
|
||||
stroke="#18181B"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Changelog
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-zinc-700 sm:grid-cols-2">
|
||||
<div>
|
||||
<a
|
||||
href="https://twitter.com/elixirphoenix"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M5.403 14c5.283 0 8.172-4.617 8.172-8.62 0-.131 0-.262-.008-.391A6.033 6.033 0 0 0 15 3.419a5.503 5.503 0 0 1-1.65.477 3.018 3.018 0 0 0 1.263-1.676 5.579 5.579 0 0 1-1.824.736 2.832 2.832 0 0 0-1.63-.916 2.746 2.746 0 0 0-1.821.319A2.973 2.973 0 0 0 8.076 3.78a3.185 3.185 0 0 0-.182 1.938 7.826 7.826 0 0 1-3.279-.918 8.253 8.253 0 0 1-2.64-2.247 3.176 3.176 0 0 0-.315 2.208 3.037 3.037 0 0 0 1.203 1.836A2.739 2.739 0 0 1 1.56 6.22v.038c0 .7.23 1.377.65 1.919.42.54 1.004.912 1.654 1.05-.423.122-.866.14-1.297.052.184.602.541 1.129 1.022 1.506a2.78 2.78 0 0 0 1.662.598 5.656 5.656 0 0 1-2.007 1.074A5.475 5.475 0 0 1 1 12.64a7.827 7.827 0 0 0 4.403 1.358" />
|
||||
</svg>
|
||||
Follow on Twitter
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://elixirforum.com"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
|
||||
</svg>
|
||||
Discuss on the Elixir Forum
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://web.libera.chat/#elixir"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.356 2.007a.75.75 0 0 1 .637.849l-1.5 10.5a.75.75 0 1 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.637ZM11.356 2.008a.75.75 0 0 1 .637.848l-1.5 10.5a.75.75 0 0 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.636Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 5.25a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75ZM13 10.75a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75Z"
|
||||
/>
|
||||
</svg>
|
||||
Chat on Libera IRC
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://discord.gg/elixir"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
|
||||
</svg>
|
||||
Join our Discord server
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://fly.io/docs/elixir/getting-started/"
|
||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
||||
>
|
||||
<path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
|
||||
</svg>
|
||||
Deploy your application
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user