7 Commits

  1. 21
      CHANGELOG.md
  2. 2
      assets/css/_forms.scss
  3. 4
      assets/css/_markdown.scss
  4. 31
      assets/css/_navigation.scss
  5. 23
      assets/css/app.scss
  6. 3
      config/dev.exs
  7. 104
      lib/mirage/lists.ex
  8. 18
      lib/mirage/lists/list.ex
  9. 55
      lib/mirage_web/live/list_live/form_component.ex
  10. 18
      lib/mirage_web/live/list_live/form_component.html.leex
  11. 46
      lib/mirage_web/live/list_live/index.ex
  12. 37
      lib/mirage_web/live/list_live/index.html.leex
  13. 21
      lib/mirage_web/live/list_live/show.ex
  14. 27
      lib/mirage_web/live/list_live/show.html.leex
  15. 10
      lib/mirage_web/router.ex
  16. 1
      lib/mirage_web/templates/layout/_header.html.eex
  17. 35
      lib/mirage_web/templates/layout/root.html.leex
  18. 2
      mix.exs
  19. 13
      priv/repo/migrations/20210227074240_create_lists.exs
  20. 66
      test/mirage/lists_test.exs
  21. 116
      test/mirage_web/live/list_live_test.exs

21
CHANGELOG.md

@ -5,6 +5,27 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v0.60.0](https://git.inhji.de/inhji/mirage/compare/v0.59.1...v0.60.0) (2021-02-27)
### Chores:
* remove $border-base variable
### Features:
* WIP sidebar
* add lists routes, to navigation
* add lists
### Bug Fixes:
* apply background to link instead of li
## [v0.59.1](https://git.inhji.de/inhji/mirage/compare/v0.59.0...v0.59.1) (2021-02-25)

2
assets/css/_forms.scss

@ -36,7 +36,7 @@ form {
@include color-assign(color, on-surface);
border: none;
border-bottom: 1px solid $border-base;
border-bottom: 1px solid white;
@include color-assign(border-color, on-surface);
margin-bottom: 1rem;

4
assets/css/_markdown.scss

@ -41,7 +41,7 @@
color: transparent;
height: 0;
width: 100%;
border-top: 1px solid $border-base;
border-top: 1px solid white;
@include color-assign(border-color, surface-var);
margin: 2rem 0;
}
@ -60,7 +60,7 @@
overflow-x: auto;
td, th {
border: 1px solid $border-base;
border: 1px solid white;
border-width: 0 0 1px;
@include color-assign(border-color, on-surface);
padding: .5em .75em;

31
assets/css/_navigation.scss

@ -47,21 +47,48 @@ aside.menu {
ul {
li {
@include color-assign(background-color, surface);
padding-left: 2rem;
margin: 0 -2rem 0 -4rem;
a {
@include color-assign(background-color, surface);
display: inline-block;
width: 100%;
padding: 1rem 2rem;
transition: all 0.3s;
text-decoration: none;
&:hover {
@include color-assign(background-color, background);
}
}
}
}
}
aside#sidebar {
position: fixed;
width: $width-sidebar;
height: 100vh;
@include color-assign(background-color, navigation);
ul {
li {
a {
@include color-assign(color, on-navigation);
display: inline-block;
padding: 1rem;
text-decoration: none;
width: 100%;
transition: all 0.3s;
&:hover {
@include color-assign(background-color, primary);
@include color-assign(color, on-primary);
}
}
}
}
}

23
assets/css/app.scss

@ -42,6 +42,8 @@ $colors: (
$font-content: 'Open Sans', Helvetica, sans-serif;
$font-heading: $font-content;
$width-sidebar: 15rem;
$bg-info: material-color('green', '500');
$color-info: material-color('green', '100');
$bg-warning: #8a6d3b;
@ -49,7 +51,7 @@ $color-warning: #fcf8e3;
$bg-danger: #a94442;
$color-danger: #f2dede;
$border-base: #666;
$shadow: 1px 1px 3px 0px rgba(50, 50, 50, 0.75);
@ -111,6 +113,14 @@ a {
.container { max-width: 60rem; }
.grid, .flex { display: flex; }
.col, .spacer { flex: 1; }
.flex-1 {flex: 1;}
.flex-2 {flex: 2;}
.flex-3 {flex: 3;}
.flex-4 {flex: 4;}
#wrapper {
margin-left: $width-sidebar;
}
main[role=main] {
padding: 0 2rem 2rem 2rem;
@ -205,7 +215,7 @@ kbd {
padding: 0 0.3rem;
margin: 0.1rem;
border-radius: 5px;
border: 1px solid $border-base;
border: 1px solid white;
@include color-assign(border-color, on-surface);
a { text-decoration: none; }
@ -330,9 +340,6 @@ article {
}
header {
// border-bottom: 1px solid $border-base;
// @include color-assign(border-color, surface-var);
@include color-assign(background, surface);
.title {
font-family: $font-heading;
@ -341,7 +348,7 @@ article {
}
footer {
border-top: 1px solid $border-base;
border-top: 1px solid white;
@include color-assign(border-color, surface-var);
}
@ -359,7 +366,7 @@ article {
margin-bottom: 1rem;
padding: 0.5rem 1rem;
font-weight: bold;
border: 1px solid $border-base;
border: 1px solid white;
@include color-assign(border-color, on-surface);
@include color-assign(color, on-primary);
@include color-assign(background-color, primary);
@ -372,7 +379,7 @@ article {
.link-wrapper {
display: flex;
padding: 1rem;
border: 1px solid $border-base;
border: 1px solid white;
@include color-assign(border-color, on-surface);
@include color-assign(background-color, surface);
margin-bottom: 1rem;

3
config/dev.exs

@ -40,6 +40,9 @@ config :git_ops,
# a section in the changelog with the header "Important Changes"
ref: [
header: "Refactors"
],
chore: [
header: "Chores"
]
],
# Instructs the tool to manage your mix version in your `mix.exs` file

104
lib/mirage/lists.ex

@ -0,0 +1,104 @@
defmodule Mirage.Lists do
@moduledoc """
The Lists context.
"""
import Ecto.Query, warn: false
alias Mirage.Repo
alias Mirage.Lists.List
@doc """
Returns the list of lists.
## Examples
iex> list_lists()
[%List{}, ...]
"""
def list_lists do
Repo.all(List)
end
@doc """
Gets a single list.
Raises `Ecto.NoResultsError` if the List does not exist.
## Examples
iex> get_list!(123)
%List{}
iex> get_list!(456)
** (Ecto.NoResultsError)
"""
def get_list!(id), do: Repo.get!(List, id)
@doc """
Creates a list.
## Examples
iex> create_list(%{field: value})
{:ok, %List{}}
iex> create_list(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_list(attrs \\ %{}) do
%List{}
|> List.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a list.
## Examples
iex> update_list(list, %{field: new_value})
{:ok, %List{}}
iex> update_list(list, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_list(%List{} = list, attrs) do
list
|> List.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a list.
## Examples
iex> delete_list(list)
{:ok, %List{}}
iex> delete_list(list)
{:error, %Ecto.Changeset{}}
"""
def delete_list(%List{} = list) do
Repo.delete(list)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking list changes.
## Examples
iex> change_list(list)
%Ecto.Changeset{data: %List{}}
"""
def change_list(%List{} = list, attrs \\ %{}) do
List.changeset(list, attrs)
end
end

18
lib/mirage/lists/list.ex

@ -0,0 +1,18 @@
defmodule Mirage.Lists.List do
use Ecto.Schema
import Ecto.Changeset
schema "lists" do
field :is_public, :boolean, default: false
field :name, :string
timestamps()
end
@doc false
def changeset(list, attrs) do
list
|> cast(attrs, [:name, :is_public])
|> validate_required([:name, :is_public])
end
end

55
lib/mirage_web/live/list_live/form_component.ex

@ -0,0 +1,55 @@
defmodule MirageWeb.ListLive.FormComponent do
use MirageWeb, :live_component
alias Mirage.Lists
@impl true
def update(%{list: list} = assigns, socket) do
changeset = Lists.change_list(list)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
end
@impl true
def handle_event("validate", %{"list" => list_params}, socket) do
changeset =
socket.assigns.list
|> Lists.change_list(list_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"list" => list_params}, socket) do
save_list(socket, socket.assigns.action, list_params)
end
defp save_list(socket, :edit, list_params) do
case Lists.update_list(socket.assigns.list, list_params) do
{:ok, _list} ->
{:noreply,
socket
|> put_flash(:info, "List updated successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
defp save_list(socket, :new, list_params) do
case Lists.create_list(list_params) do
{:ok, _list} ->
{:noreply,
socket
|> put_flash(:info, "List created successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

18
lib/mirage_web/live/list_live/form_component.html.leex

@ -0,0 +1,18 @@
<h2><%= @title %></h2>
<%= f = form_for @changeset, "#",
id: "list-form",
phx_target: @myself,
phx_change: "validate",
phx_submit: "save" %>
<%= label f, :name %>
<%= text_input f, :name %>
<%= error_tag f, :name %>
<%= label f, :is_public %>
<%= checkbox f, :is_public %>
<%= error_tag f, :is_public %>
<%= submit "Save", phx_disable_with: "Saving..." %>
</form>

46
lib/mirage_web/live/list_live/index.ex

@ -0,0 +1,46 @@
defmodule MirageWeb.ListLive.Index do
use MirageWeb, :live_view
alias Mirage.Lists
alias Mirage.Lists.List
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :lists, list_lists())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit List")
|> assign(:list, Lists.get_list!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New List")
|> assign(:list, %List{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Lists")
|> assign(:list, nil)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
list = Lists.get_list!(id)
{:ok, _} = Lists.delete_list(list)
{:noreply, assign(socket, :lists, list_lists())}
end
defp list_lists do
Lists.list_lists()
end
end

37
lib/mirage_web/live/list_live/index.html.leex

@ -0,0 +1,37 @@
<h1>Listing Lists</h1>
<%= if @live_action in [:new, :edit] do %>
<%= live_modal @socket, MirageWeb.ListLive.FormComponent,
id: @list.id || :new,
title: @page_title,
action: @live_action,
list: @list,
return_to: Routes.list_index_path(@socket, :index) %>
<% end %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Is public</th>
<th></th>
</tr>
</thead>
<tbody id="lists">
<%= for list <- @lists do %>
<tr id="list-<%= list.id %>">
<td><%= list.name %></td>
<td><%= list.is_public %></td>
<td>
<span><%= live_redirect "Show", to: Routes.list_show_path(@socket, :show, list) %></span>
<span><%= live_patch "Edit", to: Routes.list_index_path(@socket, :edit, list) %></span>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: list.id, data: [confirm: "Are you sure?"] %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= live_patch "New List", to: Routes.list_index_path(@socket, :new) %></span>

21
lib/mirage_web/live/list_live/show.ex

@ -0,0 +1,21 @@
defmodule MirageWeb.ListLive.Show do
use MirageWeb, :live_view
alias Mirage.Lists
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:list, Lists.get_list!(id))}
end
defp page_title(:show), do: "Show List"
defp page_title(:edit), do: "Edit List"
end

27
lib/mirage_web/live/list_live/show.html.leex

@ -0,0 +1,27 @@
<h1>Show List</h1>
<%= if @live_action in [:edit] do %>
<%= live_modal @socket, MirageWeb.ListLive.FormComponent,
id: @list.id,
title: @page_title,
action: @live_action,
list: @list,
return_to: Routes.list_show_path(@socket, :show, @list) %>
<% end %>
<ul>
<li>
<strong>Name:</strong>
<%= @list.name %>
</li>
<li>
<strong>Is public:</strong>
<%= @list.is_public %>
</li>
</ul>
<span><%= live_patch "Edit", to: Routes.list_show_path(@socket, :edit, @list), class: "button" %></span>
<span><%= live_redirect "Back", to: Routes.list_index_path(@socket, :index) %></span>

10
lib/mirage_web/router.ex

@ -30,6 +30,7 @@ defmodule MirageWeb.Router do
resources "/topics", TopicController
# === Links ===
live "/links", LinkLive.Index, :index
live "/links/new", LinkLive.Index, :new
live "/links/:id/edit", LinkLive.Index, :edit
@ -37,12 +38,21 @@ defmodule MirageWeb.Router do
live "/links/:id", LinkLive.Show, :show
live "/links/:id/show/edit", LinkLive.Show, :edit
# === Notes ===
live "/notes", NoteLive.Index, :index
live "/notes/new", NoteLive.Index, :new
live "/notes/:id/edit", NoteLive.Index, :edit
live "/notes/:id", NoteLive.Show, :show
live "/notes/:id/show/edit", NoteLive.Show, :edit
# === Lists ===
live "/lists", ListLive.Index, :index
live "/lists/new", ListLive.Index, :new
live "/lists/:id/edit", ListLive.Index, :edit
live "/lists/:id", ListLive.Show, :show
live "/lists/:id/show/edit", ListLive.Show, :edit
end
# Other scopes may use custom stacks.

1
lib/mirage_web/templates/layout/_header.html.eex

@ -5,6 +5,7 @@
<%= active_link(@conn, "१ Notes", to: "/notes", wrap_tag: :li) %>
<%= active_link(@conn, "२ Links", to: "/links", wrap_tag: :li) %>
<%= active_link(@conn, "३ Topics", to: "/topics", wrap_tag: :li) %>
<%= active_link(@conn, "४ Lists", to: "/lists", wrap_tag: :li) %>
<li class="spacer"></li>
<%= render "_user_menu.html", assigns %>
</ul>

35
lib/mirage_web/templates/layout/root.html.leex

@ -12,9 +12,38 @@
</head>
<body>
<div class="container">
<%= render "_header.html", assigns %>
<%= @inner_content %>
<%= render "_footer.html", assigns %>
<aside id="sidebar">
<ul>
<li>
<a href="#">Search</a>
</li>
<li>
<a href="#">Inbox</a>
</li>
<li>
<a href="/notes">Notes</a>
</li>
<li>
<a href="/links">Links</a>
</li>
<li>
<a href="/topics">Topics</a>
</li>
<li class="spacer"></li>
<li>
<a href="#">Settings</a>
</li>
<li>
<a href="#">Logout</a>
</li>
</ul>
</aside>
<div id="wrapper">
<%= render "_header.html", assigns %>
<%= @inner_content %>
<%= render "_footer.html", assigns %>
</div>
</div>
</body>
</html>

2
mix.exs

@ -1,7 +1,7 @@
defmodule Mirage.MixProject do
use Mix.Project
@version "0.59.1"
@version "0.60.0"
def project do
[

13
priv/repo/migrations/20210227074240_create_lists.exs

@ -0,0 +1,13 @@
defmodule Mirage.Repo.Migrations.CreateLists do
use Ecto.Migration
def change do
create table(:lists) do
add :name, :string
add :is_public, :boolean, default: false, null: false
timestamps()
end
end
end

66
test/mirage/lists_test.exs

@ -0,0 +1,66 @@
defmodule Mirage.ListsTest do
use Mirage.DataCase
alias Mirage.Lists
describe "lists" do
alias Mirage.Lists.List
@valid_attrs %{is_public: true, name: "some name"}
@update_attrs %{is_public: false, name: "some updated name"}
@invalid_attrs %{is_public: nil, name: nil}
def list_fixture(attrs \\ %{}) do
{:ok, list} =
attrs
|> Enum.into(@valid_attrs)
|> Lists.create_list()
list
end
test "list_lists/0 returns all lists" do
list = list_fixture()
assert Lists.list_lists() == [list]
end
test "get_list!/1 returns the list with given id" do
list = list_fixture()
assert Lists.get_list!(list.id) == list
end
test "create_list/1 with valid data creates a list" do
assert {:ok, %List{} = list} = Lists.create_list(@valid_attrs)
assert list.is_public == true
assert list.name == "some name"
end
test "create_list/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Lists.create_list(@invalid_attrs)
end
test "update_list/2 with valid data updates the list" do
list = list_fixture()
assert {:ok, %List{} = list} = Lists.update_list(list, @update_attrs)
assert list.is_public == false
assert list.name == "some updated name"
end
test "update_list/2 with invalid data returns error changeset" do
list = list_fixture()
assert {:error, %Ecto.Changeset{}} = Lists.update_list(list, @invalid_attrs)
assert list == Lists.get_list!(list.id)
end
test "delete_list/1 deletes the list" do
list = list_fixture()
assert {:ok, %List{}} = Lists.delete_list(list)
assert_raise Ecto.NoResultsError, fn -> Lists.get_list!(list.id) end
end
test "change_list/1 returns a list changeset" do
list = list_fixture()
assert %Ecto.Changeset{} = Lists.change_list(list)
end
end
end

116
test/mirage_web/live/list_live_test.exs

@ -0,0 +1,116 @@
defmodule MirageWeb.ListLiveTest do
use MirageWeb.ConnCase
import Phoenix.LiveViewTest
alias Mirage.Lists
@create_attrs %{is_public: true, name: "some name"}
@update_attrs %{is_public: false, name: "some updated name"}
@invalid_attrs %{is_public: nil, name: nil}
defp fixture(:list) do
{:ok, list} = Lists.create_list(@create_attrs)
list
end
defp create_list(_) do
list = fixture(:list)
%{list: list}
end
describe "Index" do
setup [:create_list]
test "lists all lists", %{conn: conn, list: list} do
{:ok, _index_live, html} = live(conn, Routes.list_index_path(conn, :index))
assert html =~ "Listing Lists"
assert html =~ list.name
end
test "saves new list", %{conn: conn} do
{:ok, index_live, _html} = live(conn, Routes.list_index_path(conn, :index))
assert index_live |> element("a", "New List") |> render_click() =~
"New List"
assert_patch(index_live, Routes.list_index_path(conn, :new))
assert index_live
|> form("#list-form", list: @invalid_attrs)
|> render_change() =~ "can&apos;t be blank"
{:ok, _, html} =
index_live
|> form("#list-form", list: @create_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.list_index_path(conn, :index))
assert html =~ "List created successfully"
assert html =~ "some name"
end
test "updates list in listing", %{conn: conn, list: list} do
{:ok, index_live, _html} = live(conn, Routes.list_index_path(conn, :index))
assert index_live |> element("#list-#{list.id} a", "Edit") |> render_click() =~
"Edit List"
assert_patch(index_live, Routes.list_index_path(conn, :edit, list))
assert index_live
|> form("#list-form", list: @invalid_attrs)
|> render_change() =~ "can&apos;t be blank"
{:ok, _, html} =
index_live
|> form("#list-form", list: @update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.list_index_path(conn, :index))
assert html =~ "List updated successfully"
assert html =~ "some updated name"
end
test "deletes list in listing", %{conn: conn, list: list} do
{:ok, index_live, _html} = live(conn, Routes.list_index_path(conn, :index))
assert index_live |> element("#list-#{list.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#list-#{list.id}")
end
end
describe "Show" do
setup [:create_list]
test "displays list", %{conn: conn, list: list} do
{:ok, _show_live, html} = live(conn, Routes.list_show_path(conn, :show, list))
assert html =~ "Show List"
assert html =~ list.name
end
test "updates list within modal", %{conn: conn, list: list} do
{:ok, show_live, _html} = live(conn, Routes.list_show_path(conn, :show, list))
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit List"
assert_patch(show_live, Routes.list_show_path(conn, :edit, list))
assert show_live
|> form("#list-form", list: @invalid_attrs)
|> render_change() =~ "can&apos;t be blank"
{:ok, _, html} =
show_live
|> form("#list-form", list: @update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.list_show_path(conn, :show, list))
assert html =~ "List updated successfully"
assert html =~ "some updated name"
end
end
end
Loading…
Cancel
Save