diff --git a/assets/css/app.scss b/assets/css/app.scss index ca39025..6b6e2a3 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -1,2 +1 @@ @import "bulma/bulma.sass"; - diff --git a/assets/js/app.js b/assets/js/app.js index 36a0a18..e1ec7c9 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3,6 +3,8 @@ // its own CSS file. import "../css/app.scss" +import '@fortawesome/fontawesome-free/js/all' + // webpack automatically bundles all modules in your // entry points. Those entry points can be configured // in "webpack.config.js". diff --git a/assets/package-lock.json b/assets/package-lock.json index 5dd70cb..77c4d59 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -962,6 +962,11 @@ "to-fast-properties": "^2.0.0" } }, + "@fortawesome/fontawesome-free": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz", + "integrity": "sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw==" + }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", diff --git a/assets/package.json b/assets/package.json index 5ede3a0..8a13c23 100644 --- a/assets/package.json +++ b/assets/package.json @@ -7,6 +7,7 @@ "watch": "webpack --mode development --watch" }, "dependencies": { + "@fortawesome/fontawesome-free": "^5.13.1", "bulma": "^0.9.0", "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", diff --git a/lib/planner/tasks.ex b/lib/planner/tasks.ex index be99db1..54e0ff3 100644 --- a/lib/planner/tasks.ex +++ b/lib/planner/tasks.ex @@ -4,6 +4,10 @@ defmodule Planner.Tasks do alias Planner.Tasks.Task def add_task(attrs) do + attrs = + attrs + |> cast_finished_at() + %Task{} |> Task.changeset(attrs) |> Repo.insert() @@ -14,7 +18,17 @@ defmodule Planner.Tasks do def list_unfinished_tasks do from( t in Task, - where: is_nil(t.finished_at) + where: is_nil(t.finished_at), + order_by: [desc: t.updated_at] + ) + |> Repo.all() + end + + def list_finished_tasks do + from( + t in Task, + where: not is_nil(t.finished_at), + order_by: [desc: t.updated_at] ) |> Repo.all() end @@ -24,16 +38,29 @@ defmodule Planner.Tasks do |> Task.changeset(%{}) end + def cast_finished_at(attrs) do + attrs + |> Map.update("finished_at", nil, fn + "true" -> NaiveDateTime.utc_now() + "false" -> nil + val -> val + end) + end + + def update_task(%Task{} = task, attrs) do + attrs = + attrs + |> cast_finished_at() + + task + |> Task.changeset(attrs) + |> Repo.update() + 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 index 33a4e44..375afe9 100644 --- a/lib/planner/tasks/task.ex +++ b/lib/planner/tasks/task.ex @@ -20,11 +20,4 @@ defmodule Planner.Tasks.Task do |> 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/controllers/task_controller.ex b/lib/planner_web/controllers/task_controller.ex new file mode 100644 index 0000000..8ededf3 --- /dev/null +++ b/lib/planner_web/controllers/task_controller.ex @@ -0,0 +1,82 @@ +defmodule PlannerWeb.TaskController do + use PlannerWeb, :controller + + alias Planner.Tasks + alias Planner.Tasks.Task + + def index(conn, _params) do + tasks = Tasks.list_unfinished_tasks() + + conn + |> assign(:tasks, tasks) + |> render("index.html") + end + + def new(conn, _params) do + task = %Task{} + changeset = Tasks.change_task(%Task{}) + + conn + |> assign(:task, task) + |> assign(:changeset, changeset) + |> render("new.html") + end + + def create(conn, %{"task" => task_params}) do + case Tasks.add_task(task_params) do + {:ok, _task} -> + conn + |> put_flash(:info, "task created") + |> redirect(to: Routes.task_path(conn, :index)) + + {:error, %Ecto.Changeset{} = changeset} -> + conn + |> assign(:task, %Task{}) + |> assign(:changeset, changeset) + |> render("new.html") + end + end + + def show(conn, %{"id" => id}) do + task = Tasks.get_task!(id) + + conn + |> assign(:task, task) + |> render("show.html") + end + + def edit(conn, %{"id" => id}) do + task = Tasks.get_task!(id) + changeset = Tasks.change_task(task) + + conn + |> assign(:task, task) + |> assign(:changeset, changeset) + |> render("edit.html") + end + + def update(conn, %{"id" => id, "task" => task_params}) do + task = Tasks.get_task!(id) + + case Tasks.update_task(task, task_params) do + {:ok, _task} -> + conn + |> put_flash(:info, "task updated") + |> redirect(to: Routes.task_path(conn, :index)) + + {:error, %Ecto.Changeset{} = changeset} -> + conn + |> assign(:task, task) + |> assign(:changeset, changeset) + |> render("edit.html") + end + end + + def delete(conn, %{"id" => id}) do + {:ok, _task} = Tasks.delete_task_by_id!(id) + + conn + |> put_flash(:info, "task deleted") + |> redirect(to: Routes.task_path(conn, :index)) + end +end diff --git a/lib/planner_web/live/landing_live.ex b/lib/planner_web/live/landing_live.ex index 6684198..e25b484 100644 --- a/lib/planner_web/live/landing_live.ex +++ b/lib/planner_web/live/landing_live.ex @@ -18,14 +18,14 @@ defmodule PlannerWeb.LandingLive do def render(assigns) do ~L""" <div class="box"> - <%= f = form_for(@new_task_changeset, "#", [phx_submit: :save_new_task]) %> - <div class="field"> - <div class="control"> - <%= text_input f, :value, placeholder: "add new task", class: "input" %> + <%= f = form_for(@new_task_changeset, "#", [phx_submit: :save_new_task]) %> + <div class="field"> + <div class="control"> + <%= text_input f, :value, placeholder: "add new task", class: "input" %> + </div> + <%= error_tag f, :value %> </div> - <%= error_tag f, :value %> - </div> - </form> + </form> </div> """ end diff --git a/lib/planner_web/router.ex b/lib/planner_web/router.ex index 80eecf1..4b9e2f1 100644 --- a/lib/planner_web/router.ex +++ b/lib/planner_web/router.ex @@ -54,6 +54,8 @@ defmodule PlannerWeb.Router do live("/", LandingLive, :index) + resources("/tasks", TaskController) + get("/users/settings", UserSettingsController, :edit) put("/users/settings/update_password", UserSettingsController, :update_password) put("/users/settings/update_email", UserSettingsController, :update_email) diff --git a/lib/planner_web/templates/layout/live.html.leex b/lib/planner_web/templates/layout/live.html.leex index 49376da..3d96e4d 100644 --- a/lib/planner_web/templates/layout/live.html.leex +++ b/lib/planner_web/templates/layout/live.html.leex @@ -1,17 +1,15 @@ -<section class="section"> - <div class="container"> - <%= if live_flash(@flash, :info) do %> +<main role="main" class="container"> + <%= if live_flash(@flash, :info) do %> <p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info"><%= live_flash(@flash, :info) %></p> - <% end %> + <% end %> - <%= if live_flash(@flash, :error) do %> + <%= if live_flash(@flash, :error) do %> <p class="alert alert-danger" role="alert" phx-click="lv:clear-flash" phx-value-key="error"><%= live_flash(@flash, :error) %></p> - <% end %> + <% end %> - <%= @inner_content %> - </div> + <%= @inner_content %> </main> diff --git a/lib/planner_web/templates/layout/root.html.leex b/lib/planner_web/templates/layout/root.html.leex index 11caa9b..79caea9 100644 --- a/lib/planner_web/templates/layout/root.html.leex +++ b/lib/planner_web/templates/layout/root.html.leex @@ -9,7 +9,7 @@ <script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script> </head> <body> - <nav class="navbar is-dark" role="navigation"> + <nav class="navbar is-dark mb-5" role="navigation"> <div class="navbar-brand"> <%= link "planner", to: Routes.landing_path(@conn, :index), class: "navbar-item has-text-weight-bold hast-text-info-light" %> @@ -22,7 +22,7 @@ <div id="nvbr" class="navbar-menu"> <div class="navbar-start"> - <%= link "tasks", to: Routes.landing_path(@conn, :index), class: "navbar-item" %> + <%= link "tasks", to: Routes.task_path(@conn, :index), class: "navbar-item" %> </div> <div class="navbar-end"> diff --git a/lib/planner_web/templates/task/edit.html.eex b/lib/planner_web/templates/task/edit.html.eex new file mode 100644 index 0000000..49a892a --- /dev/null +++ b/lib/planner_web/templates/task/edit.html.eex @@ -0,0 +1,5 @@ +<div class="columns"> + <div class="column is-three-fifths is-offset-one-fifth"> + <%= render "form.html", Map.put(assigns, :action, Routes.task_path(@conn, :update, @task)) %> + </div> +</div> diff --git a/lib/planner_web/templates/task/form.html.eex b/lib/planner_web/templates/task/form.html.eex new file mode 100644 index 0000000..4f1fa46 --- /dev/null +++ b/lib/planner_web/templates/task/form.html.eex @@ -0,0 +1,42 @@ +<div class="box"> + <%= form_for @changeset, @action, fn f -> %> + <%= if @changeset.action do %> + <div class="help is-danger"> + <p>something went wrong (see below)</p> + </div> + <% end %> + + <div class="field"> + <div class="control"> + <%= textarea f, :value, required: true, class: "textarea", placeholder: "task" %> + </div> + <%= error_tag f, :value %> + </div> + + <div class="field"> + <%= label f, :due_at, class: "label" do %> + due (YYYY-MM-DD HH:MM:SS) + <% end %> + <div class="control"> + <%= text_input f, :due_at, class: "input", placeholder: "YYYY-MM-DD HH:MM:SS" %> + </div> + <%= error_tag f, :due_at %> + </div> + + <div class="field"> + <%= label f, :finished_at, class: "label" do %> + <%= if is_nil(@task.finished_at) do %> + <%= checkbox f, :finished_at %> + <% else %> + <%= checkbox f, :finished_at, checked_value: @task.finished_at %> + <% end %> + finished + <% end %> + <%= error_tag f, :finished_at %> + </div> + + <div class="control"> + <%= submit "save", class: "button is-dark" %> + </div> + <% end %> +</div> diff --git a/lib/planner_web/templates/task/index.html.eex b/lib/planner_web/templates/task/index.html.eex new file mode 100644 index 0000000..06f68e5 --- /dev/null +++ b/lib/planner_web/templates/task/index.html.eex @@ -0,0 +1,33 @@ +<div class="columns"> + <div class="column table-container"> + <%= link "new", to: Routes.task_path(@conn, :new), class: "button is-dark" %> + + <div class="table-container"> + <table class="table is-striped is-hoverable is-fullwidth" style="table-layout: fixed;"> + <thead> + <tr> + <th>task</th> + </tr> + </thead> + <tbody> + <%= for task <- @tasks do %> + <tr> + <td> + <%= if not is_nil(task.due_at) do %> + <div class="tags mb-0"> + <span class="tag is-warning"> + due: <%= task.due_at %> + </span> + </div> + <% end %> + <%= link to: Routes.task_path(@conn, :show, task.id), class: "has-text-black" do %> + <%= task.value %> + <% end %> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> + </div> +</div> diff --git a/lib/planner_web/templates/task/new.html.eex b/lib/planner_web/templates/task/new.html.eex new file mode 100644 index 0000000..fd05e9e --- /dev/null +++ b/lib/planner_web/templates/task/new.html.eex @@ -0,0 +1,5 @@ +<div class="columns"> + <div class="column is-three-fifths is-offset-one-fifth"> + <%= render "form.html", Map.put(assigns, :action, Routes.task_path(@conn, :create)) %> + </div> +</div> diff --git a/lib/planner_web/templates/task/show.html.eex b/lib/planner_web/templates/task/show.html.eex new file mode 100644 index 0000000..708ced0 --- /dev/null +++ b/lib/planner_web/templates/task/show.html.eex @@ -0,0 +1,31 @@ +<div class="columns"> + <div class="column is-three-fifths is-offset-one-fifth"> + <div class="box"> + + <%= if not is_nil(@task.due_at) or is_nil(@task.filed_at) do %> + <div class="tags"> + <%= if not is_nil(@task.due_at) do %><span class="tag is-warning">due: <%= @task.due_at %></span><% end %> + <%= if is_nil(@task.filed_at) do %><span class="tag is-danger">unfiled</span><% end %> + </div> + <% end %> + + <p class="mb-5"> + <%= @task.value %> + </p> + + <div class="tags"> + <span class="tag is-light">updated: <%= @task.updated_at %></span> + <span class="tag is-light">created: <%= @task.inserted_at %></span> + </div> + + <div class="field is-grouped"> + <p class="control"> + <%= link "edit", to: Routes.task_path(@conn, :edit, @task.id), class: "button is-dark" %> + </p> + <p class="control"> + <%= link "delete", to: Routes.task_path(@conn, :delete, @task.id), class: "button is-dark", method: :delete, data: [confirm: "are you sure?"] %> + </p> + </div> + </div> + </div> +</div> diff --git a/lib/planner_web/templates/user_session/new.html.eex b/lib/planner_web/templates/user_session/new.html.eex index 602f86a..dcad0b1 100644 --- a/lib/planner_web/templates/user_session/new.html.eex +++ b/lib/planner_web/templates/user_session/new.html.eex @@ -2,7 +2,6 @@ <%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %> <%= if @error_message do %> - <div class="help is-danger"> <p><%= @error_message %></p> </div> diff --git a/lib/planner_web/views/task_view.ex b/lib/planner_web/views/task_view.ex new file mode 100644 index 0000000..52cdc23 --- /dev/null +++ b/lib/planner_web/views/task_view.ex @@ -0,0 +1,3 @@ +defmodule PlannerWeb.TaskView do + use PlannerWeb, :view +end