2022-03-28 10:26:35 +13:00
|
|
|
|
defmodule AshHqWeb.Pages.Home do
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@moduledoc "The home page"
|
|
|
|
|
|
2022-03-28 10:26:35 +13:00
|
|
|
|
use Surface.LiveComponent
|
|
|
|
|
|
2022-04-02 08:11:17 +13:00
|
|
|
|
alias AshHqWeb.Components.{CalloutText, CodeExample, SearchBar}
|
2022-08-31 10:18:34 +12:00
|
|
|
|
alias Surface.Components.Form
|
|
|
|
|
alias Surface.Components.Form.{Field, ErrorTag, TextInput, Submit}
|
2022-08-07 11:22:58 +12:00
|
|
|
|
import AshHqWeb.Components.CodeExample, only: [to_code: 1]
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-31 10:18:34 +12:00
|
|
|
|
data signed_up, :boolean, default: false
|
|
|
|
|
data email_form, :any
|
|
|
|
|
|
2022-03-28 10:26:35 +13:00
|
|
|
|
def render(assigns) do
|
|
|
|
|
~F"""
|
|
|
|
|
<div>
|
2022-07-15 07:09:04 +12:00
|
|
|
|
<div class="w-full bg-orange-600 text-center py-2 text-lg font-semibold">
|
|
|
|
|
This site is still under construction and is live for preview purposes only.
|
|
|
|
|
</div>
|
2022-08-31 05:03:29 +12:00
|
|
|
|
<div class="my-2 dark:bg-primary-black dark:bg-dark-grid bg-light-grid flex flex-col items-center pt-36">
|
|
|
|
|
<div class="text-5xl px-12 font-bold max-w-5xl mx-auto mt-2 text-center">
|
|
|
|
|
Build <CalloutText>powerful</CalloutText> and <CalloutText>composable</CalloutText> applications with a <CalloutText>flexible</CalloutText> tool-chain.
|
2022-03-28 10:26:35 +13:00
|
|
|
|
</div>
|
|
|
|
|
<div class="text-xl font-light text-gray-700 dark:text-gray-400 max-w-4xl mx-auto mt-4 text-center">
|
2022-08-31 05:03:29 +12:00
|
|
|
|
A declarative foundation for ambitious applications. Model your domain, derive the rest.
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex flex-row items-center mt-16 space-x-4">
|
|
|
|
|
<div class="flex items-center h-12 px-4 rounded-lg bg-orange-500 dark:text-white dark:hover:text-gray-200 hover:text-gray-600">
|
|
|
|
|
<a
|
|
|
|
|
href="/docs/guides/ash/latest/tutorials/get-started.md"
|
|
|
|
|
target="_blank">Get Started</a>
|
|
|
|
|
</div>
|
|
|
|
|
<SearchBar />
|
|
|
|
|
</div>
|
|
|
|
|
|
2022-08-31 10:18:34 +12:00
|
|
|
|
<div class="flex flex-row items-center mt-16 space-x-4">
|
|
|
|
|
{#if @signed_up}
|
|
|
|
|
Thank you for joining our mailing list!
|
|
|
|
|
{#else}
|
|
|
|
|
<Form for={@email_form} change="validate_email_form" submit="submit_email_form">
|
|
|
|
|
<Field name={:email}>
|
|
|
|
|
<TextInput class="text-black" opts={placeholder: "Join our mailing list for (tastefully paced) updates!"} />
|
|
|
|
|
</Field>
|
|
|
|
|
<Submit>Join</Submit>
|
|
|
|
|
</Form>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
2022-08-31 05:03:29 +12:00
|
|
|
|
<div class="pt-6 pb-6 mt-36 bg-gray-800 bg-opacity-80">
|
|
|
|
|
Through its declarative extensibility, Ash delivers more than you'd expect: Powerful APIs with filtering/sorting/pagination/calculations/aggregations, pub/sub, authorization, rich introspection, GraphQL... It's what empowers this solo developer to build an ambitious ERP!
|
|
|
|
|
- Frank Dugan III
|
|
|
|
|
System Specialist, SunnyCor Inc.
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pt-6 pb-6 mt-36 bg-gray-800 bg-opacity-80">
|
|
|
|
|
What stood out to me was how incredibly easy Ash made it for me to go from a proof of concept, to a working prototype using ETS, to a live app using Postgres.
|
|
|
|
|
- Brett Kolodny
|
|
|
|
|
Full stack engineer, MEW
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pt-6 pb-6 mt-36 bg-gray-800 bg-opacity-80">
|
|
|
|
|
Ash is such powerful idea and it gives Alembic such a massive competitive advantage that I’d be really stupid to tell anyone about it.
|
|
|
|
|
- Josh Price, Technical Director, Alembic
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pt-6 pb-6 w-full mt-36 bg-gray-800 bg-opacity-80">
|
|
|
|
|
<div class="text-5xl px-12 font-bold max-w-5xl mx-auto mt-2 text-center">
|
|
|
|
|
Why do we keep reinventing the wheel?
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pt-6 pb-6 w-full hidden sm:block mt-36">
|
|
|
|
|
<h1>Stop painting yourself into corners</h1>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="pt-6 pb-6 w-full hidden sm:block mt-36">
|
|
|
|
|
<h1>No Lock In</h1>
|
2022-03-28 10:26:35 +13:00
|
|
|
|
</div>
|
2022-04-08 18:59:39 +12:00
|
|
|
|
|
2022-04-02 08:11:17 +13:00
|
|
|
|
<div class="pt-6 pb-6 w-full hidden sm:block mt-36">
|
2022-03-30 05:12:28 +13:00
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10">
|
2022-03-28 10:26:35 +13:00
|
|
|
|
<CodeExample
|
|
|
|
|
id="define-a-resource"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
class="grow min-w-fit max-w-[1000px]"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={post_example()}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
title="Define a resource"
|
|
|
|
|
/>
|
|
|
|
|
<div class="flex flex-col space-y-8">
|
|
|
|
|
<CodeExample
|
|
|
|
|
class="w-auto"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
collapsible
|
|
|
|
|
id="use-it-programmatically"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={changeset_example()}
|
2022-03-29 08:47:43 +13:00
|
|
|
|
title="Use it programmatically"
|
2022-03-28 10:26:35 +13:00
|
|
|
|
/>
|
|
|
|
|
<CodeExample
|
|
|
|
|
class="w-auto"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
collapsible
|
2022-03-28 10:26:35 +13:00
|
|
|
|
id="graphql-interface"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={graphql_example()}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
title="Add a GraphQL interface"
|
|
|
|
|
/>
|
|
|
|
|
<CodeExample
|
|
|
|
|
class="w-auto"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
collapsible
|
2022-03-28 10:26:35 +13:00
|
|
|
|
start_collapsed
|
|
|
|
|
id="authorization-policies"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={policies_example()}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
title="Add authorization policies"
|
|
|
|
|
/>
|
|
|
|
|
<CodeExample
|
|
|
|
|
class="w-auto"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
collapsible
|
2022-03-28 10:26:35 +13:00
|
|
|
|
start_collapsed
|
|
|
|
|
id="aggregates"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={aggregate_example()}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
title="Define aggregates and calculations"
|
|
|
|
|
/>
|
|
|
|
|
<CodeExample
|
|
|
|
|
class="w-auto"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
collapsible
|
2022-03-28 10:26:35 +13:00
|
|
|
|
start_collapsed
|
|
|
|
|
id="pubsub"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={notifier_example()}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
title="Broadcast changes over Phoenix PubSub"
|
|
|
|
|
/>
|
|
|
|
|
<CodeExample
|
|
|
|
|
class="w-auto"
|
2022-03-29 08:47:43 +13:00
|
|
|
|
collapsible
|
2022-03-28 10:26:35 +13:00
|
|
|
|
start_collapsed
|
|
|
|
|
id="live-view"
|
2022-08-07 11:22:58 +12:00
|
|
|
|
code={live_view_example()}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
title="Use it with Phoenix LiveView"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
2022-08-31 10:18:34 +12:00
|
|
|
|
def mount(socket) do
|
|
|
|
|
{:ok,
|
|
|
|
|
assign(
|
|
|
|
|
socket,
|
|
|
|
|
signed_up: false,
|
|
|
|
|
email_form:
|
|
|
|
|
AshPhoenix.Form.for_create(AshHq.MailingList.Email, :create,
|
|
|
|
|
api: AshHq.MailingList,
|
|
|
|
|
upsert?: true,
|
|
|
|
|
upsert_identity: :unique_email
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("validate_email_form", %{"form" => form}, socket) do
|
|
|
|
|
{:noreply,
|
|
|
|
|
assign(socket, email_form: AshPhoenix.Form.validate(socket.assigns.email_form, form))}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("submit_email_form", _, socket) do
|
|
|
|
|
case AshPhoenix.Form.submit(socket.assigns.email_form) do
|
|
|
|
|
{:ok, _} ->
|
|
|
|
|
{:noreply, assign(socket, :signed_up, true)}
|
|
|
|
|
|
|
|
|
|
{:error, form} ->
|
|
|
|
|
{:noreply, assign(socket, email_form: form)}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@changeset_example """
|
|
|
|
|
post = Example.Post.create!(%{
|
|
|
|
|
text: "Declarative programming is fun!"
|
|
|
|
|
})
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
Example.Post.react!(post, %{type: :like})
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
Example.Post
|
|
|
|
|
|> Ash.Query.filter(likes > 10)
|
|
|
|
|
|> Ash.Query.sort(likes: :desc)
|
|
|
|
|
|> Example.read!()
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
|
|
|
|
|
defp changeset_example do
|
|
|
|
|
@changeset_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@live_view_example """
|
|
|
|
|
def mount(_params, _session, socket) do
|
|
|
|
|
form = AshPhoenix.Form.for_create(Example.Post, :create)
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
{:ok, assign(socket, :form, form}}
|
|
|
|
|
end
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
def handle_event("validate", %{"form" => input}, socket) do
|
|
|
|
|
form = AshPhoenix.Form.validate(socket.assigns.form, input)
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
{:ok, assign(socket, :form, form)}
|
|
|
|
|
end
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
def handle_event("submit", _, socket) do
|
|
|
|
|
case AshPhoenix.Form.submit(socket.assigns.form) do
|
|
|
|
|
{:ok, post} ->
|
|
|
|
|
{:ok, redirect_to_post(socket, post)}
|
2022-03-28 10:26:35 +13:00
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
{:error, form_with_errors} ->
|
|
|
|
|
{:noreply, assign(socket, :form, form_with_errors)}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
defp live_view_example do
|
|
|
|
|
@live_view_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@graphql_example """
|
|
|
|
|
graphql do
|
|
|
|
|
type :post
|
|
|
|
|
|
|
|
|
|
queries do
|
|
|
|
|
get :get_post, :read
|
|
|
|
|
list :feed, :read
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
mutations do
|
|
|
|
|
create :create_post, :create
|
|
|
|
|
update :react_to_post, :react
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
defp graphql_example do
|
|
|
|
|
@graphql_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@policies_example """
|
|
|
|
|
policies do
|
|
|
|
|
policy action_type(:read) do
|
|
|
|
|
authorize_if expr(visibility == :everyone)
|
|
|
|
|
authorize_if relates_to_actor_via([:author, :friends])
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
defp policies_example do
|
|
|
|
|
@policies_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@notifier_example """
|
|
|
|
|
pub_sub do
|
|
|
|
|
module ExampleEndpoint
|
|
|
|
|
prefix "post"
|
|
|
|
|
|
|
|
|
|
publish_all :create, ["created"]
|
|
|
|
|
publish :react, ["reaction", :id] event: "reaction"
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
defp notifier_example do
|
|
|
|
|
@notifier_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@aggregate_example """
|
|
|
|
|
aggregates do
|
|
|
|
|
count :likes, :reactions do
|
|
|
|
|
filter expr(type == :like)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
count :dislikes, :reactions do
|
|
|
|
|
filter expr(type == :dislike)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
calculations do
|
|
|
|
|
calculate :like_ratio, :float do
|
|
|
|
|
expr(likes / (likes + dislikes))
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
|
|
|
|
|
defp aggregate_example do
|
|
|
|
|
@aggregate_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
|
2022-08-07 11:22:58 +12:00
|
|
|
|
@post_example """
|
|
|
|
|
defmodule Example.Post do
|
|
|
|
|
use AshHq.Resource,
|
|
|
|
|
data_layer: AshPostgres.DataLayer
|
|
|
|
|
|
|
|
|
|
postgres do
|
|
|
|
|
table "posts"
|
|
|
|
|
repo Example.Repo
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
attributes do
|
|
|
|
|
attribute :text, :string do
|
|
|
|
|
allow_nil? false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
attribute :visibility, :atom do
|
|
|
|
|
constraints [
|
|
|
|
|
one_of: [:friends, :everyone]
|
|
|
|
|
]
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
actions do
|
|
|
|
|
update :react do
|
|
|
|
|
argument :type, Example.Types.ReactionType do
|
|
|
|
|
allow_nil? false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
change manage_relationship(
|
|
|
|
|
:type,
|
|
|
|
|
:reactions,
|
|
|
|
|
type: :append
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
relationships do
|
|
|
|
|
belongs_to :author, Example.User do
|
|
|
|
|
required? true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
has_many :reactions, Example.Reaction
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
"""
|
|
|
|
|
|> to_code()
|
|
|
|
|
|
|
|
|
|
defp post_example do
|
|
|
|
|
@post_example
|
2022-03-28 10:26:35 +13:00
|
|
|
|
end
|
|
|
|
|
end
|