diff --git a/assets/js/app.js b/assets/js/app.js index 8c02de5..296541d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -12,4 +12,20 @@ import "../css/app.scss" // import {Socket} from "phoenix" // import socket from "./socket" // -import "phoenix_html" \ No newline at end of file +import "phoenix_html" +import {Socket} from "phoenix" +import LiveSocket from "phoenix_live_view" + +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) + +// Connect if there are any LiveViews on the page +liveSocket.connect() + +// Expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) +// The latency simulator is enabled for the duration of the browser session. +// Call disableLatencySim() to disable: +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket diff --git a/assets/js/socket.js b/assets/js/socket.js deleted file mode 100644 index 09929ab..0000000 --- a/assets/js/socket.js +++ /dev/null @@ -1,63 +0,0 @@ -// NOTE: The contents of this file will only be executed if -// you uncomment its entry in "assets/js/app.js". - -// To use Phoenix channels, the first step is to import Socket, -// and connect at the socket path in "lib/web/endpoint.ex". -// -// Pass the token on params as below. Or remove it -// from the params if you are not using authentication. -import {Socket} from "phoenix" - -let socket = new Socket("/socket", {params: {token: window.userToken}}) - -// When you connect, you'll often need to authenticate the client. -// For example, imagine you have an authentication plug, `MyAuth`, -// which authenticates the session and assigns a `:current_user`. -// If the current user exists you can assign the user's token in -// the connection for use in the layout. -// -// In your "lib/web/router.ex": -// -// pipeline :browser do -// ... -// plug MyAuth -// plug :put_user_token -// end -// -// defp put_user_token(conn, _) do -// if current_user = conn.assigns[:current_user] do -// token = Phoenix.Token.sign(conn, "user socket", current_user.id) -// assign(conn, :user_token, token) -// else -// conn -// end -// end -// -// Now you need to pass this token to JavaScript. You can do so -// inside a script tag in "lib/web/templates/layout/app.html.eex": -// -// -// -// You will need to verify the user token in the "connect/3" function -// in "lib/web/channels/user_socket.ex": -// -// def connect(%{"token" => token}, socket, _connect_info) do -// # max_age: 1209600 is equivalent to two weeks in seconds -// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do -// {:ok, user_id} -> -// {:ok, assign(socket, :user, user_id)} -// {:error, reason} -> -// :error -// end -// end -// -// Finally, connect to the socket: -socket.connect() - -// Now that you are connected, you can join channels with a topic: -let channel = socket.channel("topic:subtopic", {}) -channel.join() - .receive("ok", resp => { console.log("Joined successfully", resp) }) - .receive("error", resp => { console.log("Unable to join", resp) }) - -export default socket diff --git a/assets/package-lock.json b/assets/package-lock.json index 39606c4..297f510 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -5336,6 +5336,9 @@ "phoenix_html": { "version": "file:../deps/phoenix_html" }, + "phoenix_live_view": { + "version": "file:../deps/phoenix_live_view" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", diff --git a/assets/package.json b/assets/package.json index f433fd5..5235a74 100644 --- a/assets/package.json +++ b/assets/package.json @@ -8,7 +8,8 @@ }, "dependencies": { "phoenix": "file:../deps/phoenix", - "phoenix_html": "file:../deps/phoenix_html" + "phoenix_html": "file:../deps/phoenix_html", + "phoenix_live_view": "file:../deps/phoenix_live_view" }, "devDependencies": { "@babel/core": "^7.0.0", diff --git a/lib/planner/tasks.ex b/lib/planner/tasks.ex new file mode 100644 index 0000000..be99db1 --- /dev/null +++ b/lib/planner/tasks.ex @@ -0,0 +1,39 @@ +defmodule Planner.Tasks do + import Ecto.Query + alias Planner.Repo + alias Planner.Tasks.Task + + def add_task(attrs) do + %Task{} + |> Task.changeset(attrs) + |> Repo.insert() + end + + def list_all_tasks, do: Repo.all(Task) + + def list_unfinished_tasks do + from( + t in Task, + where: is_nil(t.finished_at) + ) + |> Repo.all() + end + + def change_task(%Task{} = task) do + task + |> Task.changeset(%{}) + end + + def get_task!(id), do: Repo.get!(Task, id) + + def delete_task_by_id!(id) do + get_task!(id) + |> Repo.delete() + end + + def finish_task_by_id!(id) do + get_task!(id) + |> Task.finish_task() + |> Repo.update() + end +end diff --git a/lib/planner/tasks/task.ex b/lib/planner/tasks/task.ex new file mode 100644 index 0000000..33a4e44 --- /dev/null +++ b/lib/planner/tasks/task.ex @@ -0,0 +1,30 @@ +defmodule Planner.Tasks.Task do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "tasks" do + field(:value, :string) + field(:filed_at, :naive_datetime) + field(:finished_at, :naive_datetime) + field(:due_at, :naive_datetime) + + timestamps() + end + + @doc false + def changeset(task, attrs) do + task + |> cast(attrs, [:value, :filed_at, :finished_at, :due_at]) + |> validate_required([:value]) + |> validate_length(:value, min: 3) + end + + @doc false + def finish_task(task) do + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + # TODO, this should check if `finished_at` is not nil, first + change(task, finished_at: now) + end +end diff --git a/lib/planner_web.ex b/lib/planner_web.ex index fa002c3..4b94e96 100644 --- a/lib/planner_web.ex +++ b/lib/planner_web.ex @@ -23,6 +23,7 @@ defmodule PlannerWeb do import Plug.Conn import PlannerWeb.Gettext + import Phoenix.LiveView.Controller alias PlannerWeb.Router.Helpers, as: Routes end end @@ -35,18 +36,37 @@ defmodule PlannerWeb do # Import convenience functions from controllers import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] + import Phoenix.LiveView.Helpers # Include shared imports and aliases for views unquote(view_helpers()) end end + def live_view do + quote do + use Phoenix.LiveView, + layout: {PlannerWeb.LayoutView, "live.html"} + + unquote(view_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(view_helpers()) + end + end + def router do quote do use Phoenix.Router import Plug.Conn import Phoenix.Controller + import Phoenix.LiveView.Router end end diff --git a/lib/planner_web/controllers/page_controller.ex b/lib/planner_web/controllers/page_controller.ex deleted file mode 100644 index e5120ad..0000000 --- a/lib/planner_web/controllers/page_controller.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule PlannerWeb.PageController do - use PlannerWeb, :controller - - def index(conn, _params) do - render(conn, "index.html") - end -end diff --git a/lib/planner_web/live/landing_live.ex b/lib/planner_web/live/landing_live.ex new file mode 100644 index 0000000..04e0e74 --- /dev/null +++ b/lib/planner_web/live/landing_live.ex @@ -0,0 +1,83 @@ +defmodule PlannerWeb.LandingLive do + use Phoenix.LiveView, layout: {PlannerWeb.LayoutView, "live.html"} + use Phoenix.HTML + + import PlannerWeb.ErrorHelpers + + alias Planner.Tasks + alias Planner.Tasks.Task + + def mount(_params, _session, socket) do + socket = + socket + # |> put_flash(:info, "hello world") + |> assign(:new_task_changeset, Tasks.change_task(%Task{})) + |> assign(:tasks, Tasks.list_unfinished_tasks()) + + {:ok, socket} + end + + def render(assigns) do + ~L""" + <%= f = form_for(@new_task_changeset, "#", [phx_submit: :save_new_task]) %> + <%= label f, :value, "New Task" %> + <%= text_input f, :value %> + <%= error_tag f, :value %> + + <%= submit "Create" %> + + +
+ + + + + + + + + <%= for task <- @tasks do %> + + + + + + <% end %> + +
tasks
<%= task.value %>
+ """ + end + + def handle_event("save_new_task", %{"task" => task_params}, socket) do + case Tasks.add_task(task_params) do + {:ok, task} -> + {:noreply, + socket + |> put_flash(:info, "task created") + |> assign(:tasks, Tasks.list_unfinished_tasks())} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, + socket + |> assign(new_task_changeset: changeset)} + end + end + + def handle_event("delete_task", %{"task_id" => task_id}, socket) do + Tasks.delete_task_by_id!(task_id) + + {:noreply, + socket + |> put_flash(:info, "task deleted") + |> assign(:tasks, Tasks.list_unfinished_tasks())} + end + + def handle_event("finish_task", %{"task_id" => task_id}, socket) do + Tasks.finish_task_by_id!(task_id) + + {:noreply, + socket + |> put_flash(:info, "task completed") + |> assign(:tasks, Tasks.list_unfinished_tasks())} + end +end diff --git a/lib/planner_web/router.ex b/lib/planner_web/router.ex index 67946f0..80eecf1 100644 --- a/lib/planner_web/router.ex +++ b/lib/planner_web/router.ex @@ -6,7 +6,8 @@ defmodule PlannerWeb.Router do pipeline :browser do plug(:accepts, ["html"]) plug(:fetch_session) - plug(:fetch_flash) + plug(:fetch_live_flash) + plug(:put_root_layout, {PlannerWeb.LayoutView, :root}) plug(:protect_from_forgery) plug(:put_secure_browser_headers) plug(:fetch_current_user) @@ -51,7 +52,7 @@ defmodule PlannerWeb.Router do scope "/", PlannerWeb do pipe_through([:browser, :require_authenticated_user]) - get("/", PageController, :index) + live("/", LandingLive, :index) get("/users/settings", UserSettingsController, :edit) put("/users/settings/update_password", UserSettingsController, :update_password) diff --git a/lib/planner_web/templates/layout/app.html.eex b/lib/planner_web/templates/layout/app.html.eex index 76a1e98..09ffdad 100644 --- a/lib/planner_web/templates/layout/app.html.eex +++ b/lib/planner_web/templates/layout/app.html.eex @@ -1,28 +1,5 @@ - - - - - - - Planner ยท Phoenix Framework - "/> - - - - - -
- - - <%= @inner_content %> -
- - +
+ + + <%= @inner_content %> +
diff --git a/lib/planner_web/templates/layout/live.html.leex b/lib/planner_web/templates/layout/live.html.leex new file mode 100644 index 0000000..8dcded5 --- /dev/null +++ b/lib/planner_web/templates/layout/live.html.leex @@ -0,0 +1,11 @@ +
+ + + + + <%= @inner_content %> +
diff --git a/lib/planner_web/templates/layout/root.html.leex b/lib/planner_web/templates/layout/root.html.leex new file mode 100644 index 0000000..2fa383a --- /dev/null +++ b/lib/planner_web/templates/layout/root.html.leex @@ -0,0 +1,28 @@ + + + + + + + Planner + "/> + <%= csrf_meta_tag() %> + + + +

Planner

+ + +
+ + <%= @inner_content %> + + diff --git a/lib/planner_web/templates/page/index.html.eex b/lib/planner_web/templates/page/index.html.eex deleted file mode 100644 index e305cbe..0000000 --- a/lib/planner_web/templates/page/index.html.eex +++ /dev/null @@ -1 +0,0 @@ -

Planner

diff --git a/mix.exs b/mix.exs index db1e78f..be22b76 100644 --- a/mix.exs +++ b/mix.exs @@ -46,7 +46,9 @@ defmodule Planner.MixProject do {:telemetry_poller, "~> 0.4"}, {:gettext, "~> 0.11"}, {:jason, "~> 1.0"}, - {:plug_cowboy, "~> 2.0"} + {:plug_cowboy, "~> 2.0"}, + {:phoenix_live_view, "~> 0.13.2"}, + {:floki, ">= 0.0.0", only: :test} ] end diff --git a/mix.lock b/mix.lock index 52d4c89..3ff8eaf 100644 --- a/mix.lock +++ b/mix.lock @@ -10,7 +10,9 @@ "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, + "floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"}, "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, + "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"}, diff --git a/priv/repo/migrations/20200614031655_initial_tasks.exs b/priv/repo/migrations/20200614031655_initial_tasks.exs new file mode 100644 index 0000000..b26637d --- /dev/null +++ b/priv/repo/migrations/20200614031655_initial_tasks.exs @@ -0,0 +1,15 @@ +defmodule Planner.Repo.Migrations.InitialTasks do + use Ecto.Migration + + def change do + create table(:tasks, primary_key: false) do + add(:id, :binary_id, primary_key: true) + add(:value, :text, null: false) + add(:filed_at, :naive_datetime) + add(:finished_at, :naive_datetime) + add(:due_at, :naive_datetime) + + timestamps() + end + end +end