diff --git a/app/lib/prymn/apps.ex b/app/lib/prymn/apps.ex
new file mode 100644
index 0000000..db51b1c
--- /dev/null
+++ b/app/lib/prymn/apps.ex
@@ -0,0 +1,103 @@
+defmodule Prymn.Apps do
+ @moduledoc """
+ The Apps context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Prymn.Repo
+ alias Prymn.Apps.App
+
+ @doc """
+ Returns the list of apps.
+
+ ## Examples
+
+ iex> list_apps()
+ [%App{}, ...]
+
+ """
+ def list_apps do
+ Repo.all(App)
+ end
+
+ @doc """
+ Gets a single app.
+
+ Raises `Ecto.NoResultsError` if the App does not exist.
+
+ ## Examples
+
+ iex> get_app!(123)
+ %App{}
+
+ iex> get_app!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_app!(id), do: Repo.get!(App, id)
+
+ @doc """
+ Creates a app.
+
+ ## Examples
+
+ iex> create_app(%{field: value})
+ {:ok, %App{}}
+
+ iex> create_app(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_app(attrs \\ %{}) do
+ %App{}
+ |> App.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a app.
+
+ ## Examples
+
+ iex> update_app(app, %{field: new_value})
+ {:ok, %App{}}
+
+ iex> update_app(app, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_app(%App{} = app, attrs) do
+ app
+ |> App.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a app.
+
+ ## Examples
+
+ iex> delete_app(app)
+ {:ok, %App{}}
+
+ iex> delete_app(app)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_app(%App{} = app) do
+ Repo.delete(app)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking app changes.
+
+ ## Examples
+
+ iex> change_app(app)
+ %Ecto.Changeset{data: %App{}}
+
+ """
+ def change_app(%App{} = app, attrs \\ %{}) do
+ App.changeset(app, attrs)
+ end
+end
diff --git a/app/lib/prymn/apps/app.ex b/app/lib/prymn/apps/app.ex
new file mode 100644
index 0000000..77a9943
--- /dev/null
+++ b/app/lib/prymn/apps/app.ex
@@ -0,0 +1,17 @@
+defmodule Prymn.Apps.App do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "apps" do
+ field :name, :string
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(app, attrs) do
+ app
+ |> cast(attrs, [:name])
+ |> validate_required([:name])
+ end
+end
diff --git a/app/lib/prymn_web/components/create_app.ex b/app/lib/prymn_web/components/create_app.ex
new file mode 100644
index 0000000..cfeb448
--- /dev/null
+++ b/app/lib/prymn_web/components/create_app.ex
@@ -0,0 +1,78 @@
+defmodule PrymnWeb.CreateApp do
+ use PrymnWeb, :live_component
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+ <.wordpress_app_form :if={assigns.app_type == "wordpress"} servers={assigns[:servers]} />
+
+ """
+ end
+
+ defp wordpress_app_form(assigns) do
+ ~H"""
+ <.simple_form id="test" for={nil}>
+ <.input
+ :if={assigns.servers != nil}
+ id="server"
+ type="select"
+ name="server"
+ prompt="Select a server to host this app..."
+ options={[]}
+ value={nil}
+ label="Hosting Server"
+ />
+ <.input id="name" type="text" name="app_name" value={nil} label="WordPress Site Name" required />
+ <.input id="domain" type="text" name="domain" value={nil} label="Domain Name" required />
+ <.input type="checkbox" name="create_database?" label="Create a new database?" />
+ <.input type="text" name="db_name" value={nil} label="Database name" />
+
+ <.input type="select" name="db_name" value={nil} options={["db1", "db2"]} label="Database" />
+
+ <.input type="text" name="admin_username" value={nil} label="Admin Username" />
+ <.input type="email" name="admin_email" value={nil} label="Admin Email" />
+ <.input type="password" name="admin_password" value={nil} label="Admin Password" />
+
+ <.button type="submit">Create
+
+ """
+ end
+end
diff --git a/app/lib/prymn_web/live/app_index_live.ex b/app/lib/prymn_web/live/app_index_live.ex
new file mode 100644
index 0000000..76ad2d7
--- /dev/null
+++ b/app/lib/prymn_web/live/app_index_live.ex
@@ -0,0 +1,63 @@
+defmodule PrymnWeb.AppIndexLive do
+ use PrymnWeb, :live_view
+
+ @impl true
+ def mount(_, _, socket) do
+ apps = Prymn.Apps.list_apps()
+ servers = Prymn.Servers.list_servers()
+
+ {:ok,
+ socket
+ |> assign(:servers, servers)
+ |> assign(:apps, apps)}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <%= cond do %>
+ <% assigns.live_action == :new -> %>
+ <.back navigate={~p"/apps"}>Go back
+ <.live_component
+ id={:new}
+ module={PrymnWeb.CreateApp}
+ app_type={assigns[:app_type]}
+ servers={@servers}
+ />
+ <% assigns.apps == [] -> %>
+ <.onboarding />
+ <% true -> %>
+
+ <.header>
+ Live Apps
+ <:subtitle>
+ All of your apps accross all projects.
+
+
+
+ <% end %>
+ """
+ end
+
+ @impl true
+ def handle_params(%{"app_type" => app_type}, _, socket) do
+ {:noreply, assign(socket, app_type: app_type)}
+ end
+
+ def handle_params(_, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, (socket.assigns.live_action == :new && "New App") || "Apps")}
+ end
+
+ defp onboarding(assigns) do
+ ~H"""
+
+
You have no Apps.
+ <.button type="link" patch={~p"/apps/new"}>
+ <.icon class="mr-2 h-6 w-6" name="hero-plus" /> Create your first App!
+
+
+ """
+ end
+end
diff --git a/app/lib/prymn_web/router.ex b/app/lib/prymn_web/router.ex
index 21ccd5c..ef30200 100644
--- a/app/lib/prymn_web/router.ex
+++ b/app/lib/prymn_web/router.ex
@@ -37,6 +37,9 @@ defmodule PrymnWeb.Router do
live "/servers", ServerLive.Index, :index
live "/servers/new", ServerLive.Index, :new
live "/servers/:id", ServerLive.Show
+
+ live "/apps", AppIndexLive
+ live "/apps/new", AppIndexLive, :new
end
end
diff --git a/app/test/prymn/apps_test.exs b/app/test/prymn/apps_test.exs
new file mode 100644
index 0000000..1d31f6e
--- /dev/null
+++ b/app/test/prymn/apps_test.exs
@@ -0,0 +1,59 @@
+defmodule Prymn.AppsTest do
+ use Prymn.DataCase
+
+ alias Prymn.Apps
+
+ describe "apps" do
+ alias Prymn.Apps.App
+
+ import Prymn.AppsFixtures
+
+ @invalid_attrs %{name: nil}
+
+ test "list_apps/0 returns all apps" do
+ app = app_fixture()
+ assert Apps.list_apps() == [app]
+ end
+
+ test "get_app!/1 returns the app with given id" do
+ app = app_fixture()
+ assert Apps.get_app!(app.id) == app
+ end
+
+ test "create_app/1 with valid data creates a app" do
+ valid_attrs = %{name: "some name"}
+
+ assert {:ok, %App{} = app} = Apps.create_app(valid_attrs)
+ assert app.name == "some name"
+ end
+
+ test "create_app/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Apps.create_app(@invalid_attrs)
+ end
+
+ test "update_app/2 with valid data updates the app" do
+ app = app_fixture()
+ update_attrs = %{name: "some updated name"}
+
+ assert {:ok, %App{} = app} = Apps.update_app(app, update_attrs)
+ assert app.name == "some updated name"
+ end
+
+ test "update_app/2 with invalid data returns error changeset" do
+ app = app_fixture()
+ assert {:error, %Ecto.Changeset{}} = Apps.update_app(app, @invalid_attrs)
+ assert app == Apps.get_app!(app.id)
+ end
+
+ test "delete_app/1 deletes the app" do
+ app = app_fixture()
+ assert {:ok, %App{}} = Apps.delete_app(app)
+ assert_raise Ecto.NoResultsError, fn -> Apps.get_app!(app.id) end
+ end
+
+ test "change_app/1 returns a app changeset" do
+ app = app_fixture()
+ assert %Ecto.Changeset{} = Apps.change_app(app)
+ end
+ end
+end
diff --git a/app/test/support/fixtures/apps_fixtures.ex b/app/test/support/fixtures/apps_fixtures.ex
new file mode 100644
index 0000000..83e103f
--- /dev/null
+++ b/app/test/support/fixtures/apps_fixtures.ex
@@ -0,0 +1,20 @@
+defmodule Prymn.AppsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Prymn.Apps` context.
+ """
+
+ @doc """
+ Generate a app.
+ """
+ def app_fixture(attrs \\ %{}) do
+ {:ok, app} =
+ attrs
+ |> Enum.into(%{
+ name: "some name"
+ })
+ |> Prymn.Apps.create_app()
+
+ app
+ end
+end
diff --git a/priv/repo/migrations/20231119141943_create_apps.exs b/priv/repo/migrations/20231119141943_create_apps.exs
new file mode 100644
index 0000000..7b1e392
--- /dev/null
+++ b/priv/repo/migrations/20231119141943_create_apps.exs
@@ -0,0 +1,11 @@
+defmodule Prymn.Repo.Migrations.CreateApps do
+ use Ecto.Migration
+
+ def change do
+ create table(:apps) do
+ add :name, :string
+
+ timestamps()
+ end
+ end
+end