52 Commits
Author SHA1 Message Date
KKlochko 669a288ef1 Update the version.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-06-04 12:49:14 +03:00
KKlochko b47d994989 Add the JSON API route to search by BookID. 2025-06-04 12:42:04 +03:00
KKlochko dd06829f31 Update the generator for BookIds.
continuous-integration/drone/push Build is passing
2025-06-03 20:35:48 +03:00
KKlochko 1842ebc32c Update the tests for Metadata.get_author_book action. 2025-06-03 19:40:22 +03:00
KKlochko 02a34694e4 Update the Author Show LiveView to show their books. 2025-06-03 19:32:17 +03:00
KKlochko e4f17524ea Update the card components to check a dbi_server existence by the id. 2025-06-03 19:30:00 +03:00
KKlochko 03b889b094 Fix to hide the label if no identifiers for Book Show LiveView. 2025-06-03 18:09:08 +03:00
KKlochko 30df53702b Update to allow a Book to have no Publisher. 2025-06-03 18:06:26 +03:00
KKlochko cedc4d78e8 Update DBIServer Show LiveView to show the edit button for admins. 2025-06-03 18:03:19 +03:00
KKlochko 11a7962167 Update the code format for components and LiveViews.
continuous-integration/drone/push Build is failing
2025-06-03 10:55:19 +03:00
KKlochko 589f13d90e Update the Book's nested form's layout if there're an error. 2025-06-03 10:50:14 +03:00
KKlochko e754ef0d8f Update the DarkModeSwitcher component to change the icon order. 2025-06-03 10:48:59 +03:00
KKlochko aded500cab Add the DarkModeSwitcher component. 2025-06-02 22:02:38 +03:00
KKlochko 3bc4616c5d Update to allow search book by an empty bid.
continuous-integration/drone/push Build is passing
2025-06-01 20:47:34 +03:00
KKlochko 5c31715211 Update the background colors for the light theme.
continuous-integration/drone/push Build is passing
2025-06-01 11:47:53 +03:00
KKlochko 2e023fa2f7 Fix the light theme for Author Show LiveView. 2025-06-01 11:46:28 +03:00
KKlochko e9e49572d1 Add the content's horizontal padding for card components.
continuous-integration/drone/push Build is passing
2025-05-31 21:43:42 +03:00
KKlochko bf333aad07 Update the select components to show the message about no records. 2025-05-31 21:39:28 +03:00
KKlochko e226e696da Update the select components to add hover style for records. 2025-05-31 21:36:05 +03:00
KKlochko ac4e28bd56 Update the README to add step to choose a pre-build or build image.
continuous-integration/drone/push Build is passing
2025-05-29 20:37:19 +03:00
KKlochko 59ab26a838 Update the docker-compose to use the pre-built image by default. 2025-05-29 20:36:44 +03:00
KKlochko 045faf59a4 Update the CI/CD configuration to build the image.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-05-27 21:44:40 +03:00
KKlochko 152bee732f Update the version. 2025-05-27 21:43:51 +03:00
KKlochko 99188454e9 Update Navbar partial to show the Server menu item for moderators.
continuous-integration/drone/push Build is passing
2025-05-26 20:38:19 +03:00
KKlochko 80e99b7de8 Remove extra debug logs which used to inspect a state.
continuous-integration/drone/push Build is passing
2025-05-26 14:30:53 +03:00
KKlochko 51c584056b Fix a typo in SearchResources component that an admin is a moderator.
continuous-integration/drone/push Build is passing
2025-05-26 14:21:59 +03:00
KKlochko b70b99ba51 Update DBIServer's Index and Show to allow moderators to read. 2025-05-26 14:21:14 +03:00
KKlochko 9c7a555dae Update Show LiveViews to add information from which server. 2025-05-26 14:19:38 +03:00
KKlochko ab3d65a899 Update Oban configuration to prune jobs which older than 7 days.
continuous-integration/drone/push Build is passing
2025-05-25 21:26:22 +03:00
KKlochko 44841a53e8 Update the LiveViews permission test to fix misuse of the user values.
continuous-integration/drone/push Build is passing
2025-05-24 21:56:49 +03:00
KKlochko a25a4e24da Update to refatactor tests for syncing. 2025-05-24 21:52:22 +03:00
KKlochko 493aca0e5b Update to move get_id functions to support/helpers. 2025-05-24 21:46:02 +03:00
KKlochko 8a3b522049 Update to refactor tests for Author and Book. 2025-05-24 21:21:04 +03:00
KKlochko 89c871110c Update to refactor tests for resource forms. 2025-05-24 21:19:19 +03:00
KKlochko 64028d09f5 Fix data transformers to remove a nil value of dbi_server for syncing.
continuous-integration/drone/push Build is passing
Map.put_new don't update nil value, because the key exists.
2025-05-23 12:50:43 +03:00
KKlochko ea5ada751d Update DBIServerTransformer to fetch the dbi_server relationship. 2025-05-23 12:47:46 +03:00
KKlochko f4eb929401 Update DBIServer to make the relationship to another server public. 2025-05-23 12:42:39 +03:00
KKlochko 59e2d2f8a1 Update the footer to add the license.
continuous-integration/drone/push Build is passing
2025-05-22 21:38:29 +03:00
KKlochko ddcab0b843 Update the API get actions to use the by_id action.
continuous-integration/drone/push Build is passing
2025-05-21 19:42:13 +03:00
KKlochko b7979bf6ba Add descriptions for resources' actions. 2025-05-21 19:41:28 +03:00
KKlochko db025ece6a Fix DBIServer FormComponent to allow edit only other servers' sync_on?.
continuous-integration/drone/push Build is passing
2025-05-20 22:13:10 +03:00
KKlochko ffbec934a6 Update resources and generator to accept dbi_server_id.
continuous-integration/drone/push Build is passing
2025-05-19 22:09:22 +03:00
KKlochko 4edf526475 Update tests to add missing assert and = which check permission for LiveViews.
continuous-integration/drone/push Build is failing
2025-05-18 22:22:01 +03:00
KKlochko 4a28708479 Add tests to check permissions for editing other servers' data. 2025-05-18 22:19:11 +03:00
KKlochko 3430693dac Update the pagination component to show itself if there's more than one page.
continuous-integration/drone/push Build is failing
2025-05-17 22:10:59 +03:00
KKlochko 54f15bead3 Rename the test module to LiveViewsPermissionsTest. 2025-05-17 22:09:54 +03:00
KKlochko 88a8eba322 Update to refactor tests which check permission for LiveViews.
continuous-integration/drone/push Build is passing
2025-05-16 13:10:25 +03:00
KKlochko f406239b16 Add tests to check permission for Show and Edit LiveViews.
continuous-integration/drone/push Build is passing
2025-05-15 11:50:33 +03:00
KKlochko bdc6670475 Add tests to check permission for Index LiveViews.
continuous-integration/drone/push Build is passing
2025-05-14 14:43:15 +03:00
KKlochko 4688d09dcf Add the LiveCase template for testing LiveViews. 2025-05-14 14:42:47 +03:00
KKlochko c51bca1eb5 Add the helper to make a user log in. 2025-05-14 14:41:42 +03:00
KKlochko 7bf009c38f Update the error message for the RequireAdmin Plug. 2025-05-14 14:41:10 +03:00
76 changed files with 2199 additions and 456 deletions
+15 -1
View File
@@ -24,6 +24,20 @@ steps:
commands:
- mix test
- name: build the image
image: plugins/kaniko
settings:
username: kklochko
password:
from_secret: REGISTRY_PASSWORD
repo: git.kklochko.space/kklochko/decentralised_book_index
registry: git.kklochko.space
tags:
- latest
- ${DRONE_TAG}
when:
event: tag
volumes:
- name: mix
temp: {}
@@ -34,4 +48,4 @@ services:
environment:
POSTGRES_DB: decentralised_book_index_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: postgres
+3
View File
@@ -9,6 +9,9 @@
- change the values in `.env` and `.env_db`
you can use `mix phx.gen.secret` to generate secrets.
update the PHX_HOST to your domain name.
- if you don't want to pull the pre-built container
- uncomment the part to build the image in the `docker-compose.yaml` file
- comment the image part to avoid pulling.
- run `docker compose up -d`
- run migrations `docker exec -it dbi bin/migrate`
- open the url (`PHX_HOST`) in a browser.
+4 -1
View File
@@ -26,11 +26,14 @@ import "flowbite/dist/flowbite.phoenix.js";
import bidSort from "./bidSort";
import authorRoleSort from "./authorRoleSort";
import darkModeHook from "../vendor/dark_mode"
let DarkThemeToggle = darkModeHook;
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: { bidSort, authorRoleSort }
hooks: { bidSort, authorRoleSort, DarkThemeToggle }
})
// Show progress bar on live navigation and form submits
+1
View File
@@ -6,6 +6,7 @@ const fs = require("fs")
const path = require("path")
module.exports = {
darkMode: 'class',
content: [
"../deps/ash_authentication_phoenix/**/*.*ex",
"./js/**/*.js",
+39
View File
@@ -0,0 +1,39 @@
const localStorageKey = 'theme';
const isDark = () => {
if (localStorage.getItem(localStorageKey) === 'dark') return true;
if (localStorage.getItem(localStorageKey) === 'light') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
const setupThemeToggle = () => {
toggleVisibility = (dark) => {
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon == null || themeToggleLightIcon == null) return;
const show = dark ? themeToggleLightIcon : themeToggleDarkIcon;
const hide = dark ? themeToggleDarkIcon : themeToggleLightIcon;
show.classList.remove('hidden', 'text-transparent');
hide.classList.add('hidden', 'text-transparent');
if (dark) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
try { localStorage.setItem(localStorageKey, dark ? 'dark' : 'light') } catch (_err) { }
}
toggleVisibility(isDark())
document.getElementById('theme-toggle').addEventListener('click', function () {
toggleVisibility(!isDark())
});
}
const darkModeHook = {
mounted() {
setupThemeToggle();
},
updated() {
},
}
export default darkModeHook;
+2 -1
View File
@@ -12,7 +12,8 @@ config :decentralised_book_index, Oban,
repo: DecentralisedBookIndex.Repo,
queues: [default: 14],
plugins: [
Oban.Plugins.Pruner,
# A job will be pruned after 7 days.
{Oban.Plugins.Pruner, max_age: 7*24*60*60},
{Oban.Plugins.Cron,
crontab: [
{"@daily", DecentralisedBookIndex.SyncWorker},
+4 -2
View File
@@ -17,8 +17,10 @@ services:
web:
# to build the image
build:
context: .
#build:
# context: .
# to pull the pre-built image
image: git.kklochko.space/kklochko/decentralised_book_index:latest
container_name: dbi
ports:
- 4000:4000
+5 -4
View File
@@ -6,8 +6,9 @@ defmodule DecentralisedBookIndex.Metadata do
json_api do
routes do
base_route "/books", Metadata.Book do
get :read
get :by_id
index :search
index :search_by_bid, route: "/search-by-bid"
related :bids, :read, primary?: true
related :author_roles, :read, primary?: true
@@ -15,17 +16,17 @@ defmodule DecentralisedBookIndex.Metadata do
end
base_route "/authors", Metadata.Author do
get :read
get :by_id
index :search
end
base_route "/publishers", Metadata.Publisher do
get :read
get :by_id
index :search
end
base_route "/servers", Metadata.DBIServer do
get :read
get :by_id
index :search
end
end
@@ -25,12 +25,16 @@ defmodule DecentralisedBookIndex.Metadata.Author do
end
end
resource do
description "An author's metadata."
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:name, :description, :avatar_url, :author_alias_registry_id]
accept [:name, :description, :avatar_url, :author_alias_registry_id, :dbi_server_id]
change fn changeset, context ->
actor = Map.get(context, :actor, nil)
@@ -100,6 +104,7 @@ defmodule DecentralisedBookIndex.Metadata.Author do
end
read :by_id do
description "Return the Author by its id."
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
@@ -136,7 +141,10 @@ defmodule DecentralisedBookIndex.Metadata.Author do
end
read :search do
description "Return a list of Authors, optionally filtering by name."
argument :name, :ci_string do
description "Return Authors, which names includes the name."
constraints allow_empty?: true
default ""
end
@@ -21,8 +21,12 @@ defmodule DecentralisedBookIndex.Metadata.AuthorRole do
end
end
resource do
description "A metadata of an author's role."
end
actions do
defaults [:read, :destroy]
defaults [:destroy]
create :create do
primary? true
@@ -43,13 +47,20 @@ defmodule DecentralisedBookIndex.Metadata.AuthorRole do
end
end
read :read do
description "Return a list of AuthorRoles."
primary? true
end
read :by_id do
description "Return the AuthorRole by its id."
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
end
read :by_book_id do
description "Return the AuthorRole by its BookId."
argument :book_id, :uuid, allow_nil?: false
filter expr(book_id == ^arg(:book_id))
end
+19 -4
View File
@@ -25,6 +25,10 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end
end
resource do
description "A book's metadata."
end
actions do
defaults [:read, :destroy]
@@ -142,6 +146,7 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end
read :by_id do
description "Return the Book by its id."
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
@@ -174,15 +179,24 @@ defmodule DecentralisedBookIndex.Metadata.Book do
prepare fn query, context ->
author = query.arguments.author
{:ok, author_ids} = Metadata.get_author_ids(author)
author_ids =
case Metadata.get_author_ids(author) do
{:ok, ids} -> ids
{:error, _error} -> []
end
Metadata.Book
query
|> Ash.Query.filter(expr(exists(author_roles, author_id in ^author_ids)))
end
pagination offset?: true, default_limit: 10
end
read :search do
description "Return a list of Books, optionally filtering by title."
argument :query, :ci_string do
description "Return Books, which titles includes the query."
constraints allow_empty?: true
default ""
end
@@ -193,8 +207,10 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end
read :search_by_bid do
description "Return a list of Books, optionally filtering by BookId."
argument :type, :string, allow_nil?: false
argument :bid, :string, allow_nil?: false
argument :bid, :string, allow_nil?: true
filter expr(exists(bids, type == ^arg(:type) and contains(bid, ^arg(:bid))))
@@ -363,7 +379,6 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end
belongs_to :publisher, Metadata.Publisher do
allow_nil? false
public? true
end
@@ -26,8 +26,12 @@ defmodule DecentralisedBookIndex.Metadata.BookId do
end
end
resource do
description "A book's identifier."
end
actions do
defaults [:read, :destroy]
defaults [:destroy]
create :create do
primary? true
@@ -39,7 +43,13 @@ defmodule DecentralisedBookIndex.Metadata.BookId do
accept [:id, :type, :bid, :order]
end
read :read do
description "Return a list of BookIds."
primary? true
end
read :by_id do
description "Return the BookId by its id."
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
@@ -21,12 +21,16 @@ defmodule DecentralisedBookIndex.Metadata.DBIServer do
end
end
resource do
description "An DBIServer's metadata."
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:name, :url, :sync_on?]
accept [:name, :url, :sync_on?, :dbi_server_id]
end
create :sync_create do
@@ -43,13 +47,17 @@ defmodule DecentralisedBookIndex.Metadata.DBIServer do
end
read :by_id do
description "Return the DBIServer by its id."
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
end
read :search do
description "Return a list of DBIServers, optionally filtering by name."
argument :name, :ci_string do
description "Return DBIServers, which names includes the name."
constraints allow_empty?: true
default ""
end
@@ -95,7 +103,9 @@ defmodule DecentralisedBookIndex.Metadata.DBIServer do
end
relationships do
belongs_to :dbi_server, Metadata.DBIServer
belongs_to :dbi_server, Metadata.DBIServer do
public? true
end
has_many :author, Metadata.Author
has_many :author_alias_registries, Metadata.AuthorAliasRegistry
@@ -22,26 +22,39 @@ defmodule DecentralisedBookIndex.Metadata.Publisher do
end
end
resource do
description "An publisher's metadata."
end
actions do
defaults [:read, :destroy]
defaults [:destroy]
create :create do
primary? true
accept [:name]
accept [:name, :dbi_server_id]
end
create :sync_create do
accept [:id, :name, :inserted_at, :updated_at, :dbi_server_id]
end
read :read do
description "Return a list of Publishers."
primary? true
end
read :by_id do
description "Return the Publisher by its id."
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
end
read :search do
description "Return a list of Publishers, optionally filtering by name."
argument :name, :ci_string do
description "Return Publishers, which names includes the name."
constraints allow_empty?: true
default ""
end
@@ -20,6 +20,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorTransformer do
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
}
{:ok, attrs}
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs}
end
end
end
@@ -26,6 +26,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookTransformer do
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
}
{:ok, attrs}
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs}
end
end
end
@@ -13,9 +13,15 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformer do
name: get_in(json_body, ["attributes", "name"]),
url: get_in(json_body, ["attributes", "url"]),
inserted_at: get_in(json_body, ["attributes", "inserted_at"]),
updated_at: get_in(json_body, ["attributes", "updated_at"])
updated_at: get_in(json_body, ["attributes", "updated_at"]),
# relationship
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
}
{:ok, attrs}
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs}
end
end
end
@@ -17,6 +17,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.PublisherTransformer do
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
}
{:ok, attrs}
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs}
end
end
end
@@ -50,7 +50,11 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
>
<div id={"#{@id}-bg"} class="bg-zinc-50/90 dark:bg-gray-900/90 fixed inset-0 transition-opacity" aria-hidden="true" />
<div
id={"#{@id}-bg"}
class="bg-zinc-50/90 dark:bg-gray-900/90 fixed inset-0 transition-opacity"
aria-hidden="true"
/>
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
@@ -425,7 +429,8 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem] dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400 dark:border-rose-400 dark:focus:border-rose-400"
@errors != [] &&
"border-rose-400 focus:border-rose-400 dark:border-rose-400 dark:focus:border-rose-400"
]}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
@@ -449,7 +454,8 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400 dark:border-rose-400 dark:focus:border-rose-400"
@errors != [] &&
"border-rose-400 focus:border-rose-400 dark:border-rose-400 dark:focus:border-rose-400"
]}
{@rest}
/>
@@ -546,8 +552,8 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0 ">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 relative shadow-md sm:rounded-lg overflow-hidden">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 bg-slate-100 dark:bg-gray-800 relative shadow-md sm:rounded-lg overflow-hidden">
<thead class="text-xs text-gray-700 uppercase bg-slate-200 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th :for={col <- @col} class="p-4 font-normal">{col[:label]}</th>
<th :if={@action != []} class="relative p-0 pb-4">
@@ -560,7 +566,11 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-zinc-100 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class=" bg-white shadow-md dark:bg-gray-800 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-700">
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class="bg-slate-100 shadow-md dark:bg-gray-800 hover:bg-slate-300 dark:bg-gray-850 dark:hover:bg-gray-700"
>
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
@@ -568,7 +578,10 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class={["p-4 relative font-sm text-gray-500 dark:text-gray-400", i == 0 && "p-4 medium text-gray-900 whitespace-nowrap dark:text-white"]}>
<span class={[
"p-4 relative font-sm text-gray-500 dark:text-gray-400",
i == 0 && "p-4 medium text-gray-900 whitespace-nowrap dark:text-white"
]}>
{render_slot(col, @row_item.(row))}
</span>
</div>
@@ -10,6 +10,14 @@
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
</head>
<body class="bg-white dark:bg-gray-900">
{@inner_content}
@@ -34,6 +34,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents do
alias MyComponents.SelectAuthorAlias
import MyComponents.SelectedAuthorAlias, only: [selected_author_alias: 1]
import MyComponents.DarkModeSwitcher, only: [dark_mode_switcher: 1]
end
end
end
@@ -9,8 +9,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.AuthorCard do
def author_card(assigns) do
~H"""
<div class="w-full max-w-sm bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) and is_nil(@author.dbi_server) do %>
<div class="w-full max-w-sm bg-slate-100 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) and is_nil(@author.dbi_server_id) do %>
<div class="flex justify-end px-4 pt-4">
<button
id={"dropdownButton#{@author.id}"}
@@ -32,7 +32,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.AuthorCard do
<!-- Dropdown menu -->
<div
id={"dropdown#{@author.id}"}
class="z-10 hidden text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700"
class="z-10 hidden text-base list-none bg-slate-200 divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700"
>
<ul class="py-2" aria-labelledby="dropdownButton">
<li>
@@ -63,7 +63,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.AuthorCard do
</div>
</div>
<% end %>
<div class="flex flex-col items-center py-5 text-center">
<div class="flex flex-col items-center py-5 text-center px-2">
<.link navigate={~p"/authors/#{@author.id}"}>
<%= if @author.avatar_url != nil do %>
<img
@@ -9,8 +9,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookCard do
def book_card(assigns) do
~H"""
<div class="w-full max-w-sm bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) and is_nil(@book.dbi_server) do %>
<div class="w-full max-w-sm bg-slate-100 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) and is_nil(@book.dbi_server_id) do %>
<div class="flex justify-end px-4 pt-4">
<button
id={"dropdownButton#{@book.id}"}
@@ -32,7 +32,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookCard do
<!-- Dropdown menu -->
<div
id={"dropdown#{@book.id}"}
class="z-10 hidden text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700"
class="z-10 hidden text-base list-none bg-slate-200 divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700"
>
<ul class="py-2" aria-labelledby="dropdownButton">
<li>
@@ -63,7 +63,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookCard do
</div>
</div>
<% end %>
<div class="flex flex-col items-center py-5 text-center">
<div class="flex flex-col items-center py-5 text-center px-2">
<.link navigate={~p"/books/#{@book.id}"}>
<%= if @book.cover_image_url != nil do %>
<img class="w-36 mb-3 shadow-lg" src={@book.cover_image_url} alt={"#{@book.title} image"} />
@@ -14,37 +14,69 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
def book_search(assigns) do
~H"""
<div class="bg-white shadow-md dark:bg-gray-800 sm:rounded-lg">
<div class="bg-slate-100 shadow-md dark:bg-gray-800 sm:rounded-lg">
<div class="flex items-center justify-center p-4 flex-row space-y-0 space-x-4">
<div class="w-full grow">
<form class="flex items-center" phx-change="search" phx-submit="search">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
<svg
aria-hidden="true"
class="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewbox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
</div>
<input name="query" type="text" phx-debounce="300" autocomplete="off" id="simple-search" class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder="Search" value={@search_query}>
<input
name="query"
type="text"
phx-debounce="300"
autocomplete="off"
id="simple-search"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
value={@search_query}
/>
</div>
</form>
</div>
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<.link patch={~p"/books/new"} class="flex-shrink-0 my-auto">
<button type="button" class="flex items-center justify-between px-4 py-2 text-sm font-medium text-black dark:text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
<svg class="h-3.5 w-3.5 mr-2" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path clip-rule="evenodd" fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
</svg>
Add book
</button>
</.link>
<% end %>
<.dropdown_settings
search_mode={@search_mode}
search_mode_options={@search_mode_options}
select_options={@select_options}
selected_option={@selected_option}
/>
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<.link patch={~p"/books/new"} class="flex-shrink-0 my-auto">
<button
type="button"
class="flex items-center justify-between px-4 py-2 text-sm font-medium text-black dark:text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800"
>
<svg
class="h-3.5 w-3.5 mr-2"
fill="currentColor"
viewbox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clip-rule="evenodd"
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
/>
</svg>
Add book
</button>
</.link>
<% end %>
<.dropdown_settings
search_mode={@search_mode}
search_mode_options={@search_mode_options}
select_options={@select_options}
selected_option={@selected_option}
/>
</div>
</div>
"""
@@ -58,17 +90,42 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
def dropdown_settings(assigns) do
~H"""
<div class="flex justify-end px-4">
<button id="dropdownButtonSearchBook" data-dropdown-toggle="dropdownSearchBook" class="inline-block text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-1.5" type="button">
<span class="sr-only">Open dropdown</span>
<svg class="w-5 h-5 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M20 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6h-2m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4"/>
</svg>
</button>
<!-- Dropdown menu -->
<div id="dropdownSearchBook" class="z-10 hidden text-base list-none bg-white rounded-lg shadow-sm w-64 dark:bg-gray-700 p-2 flex flex-col gap-2">
<div class="flex items-center space-x-3 md:w-auto">
<form phx-change="change-search-mode" class="w-full">
<select name="search_mode" id="search_mode" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<button
id="dropdownButtonSearchBook"
data-dropdown-toggle="dropdownSearchBook"
class="inline-block text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-1.5"
type="button"
>
<span class="sr-only">Open dropdown</span>
<svg
class="w-5 h-5 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
d="M20 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6h-2m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4"
/>
</svg>
</button>
<!-- Dropdown menu -->
<div
id="dropdownSearchBook"
class="z-10 hidden text-base list-none bg-slate-200 rounded-lg shadow-sm w-64 dark:bg-gray-700 p-2 flex flex-col gap-2"
>
<div class="flex items-center space-x-3 md:w-auto">
<form phx-change="change-search-mode" class="w-full">
<select
name="search_mode"
id="search_mode"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<%= for {name, value} <- @search_mode_options do %>
<%= if value == @search_mode do %>
<option value={value} selected>{name}</option>
@@ -77,11 +134,15 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
<% end %>
<% end %>
</select>
</form>
</div>
<div class="flex items-center space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full">
<select name="sort_by" id="sort_by" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
</form>
</div>
<div class="flex items-center space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full">
<select
name="sort_by"
id="sort_by"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %>
<option value={value} selected>{name}</option>
@@ -90,9 +151,9 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
<% end %>
<% end %>
</select>
</form>
</div>
</form>
</div>
</div>
</div>
"""
end
@@ -120,9 +120,21 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Buttons do
]}
{@rest}
>
<svg class="w-6 h-6 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M5 3a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7.414A2 2 0 0 0 20.414 6L18 3.586A2 2 0 0 0 16.586 3H5Zm3 11a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6H8v-6Zm1-7V5h6v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1Z" clip-rule="evenodd"/>
<path fill-rule="evenodd" d="M14 17h-4v-2h4v2Z" clip-rule="evenodd"/>
<svg
class="w-6 h-6 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M5 3a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7.414A2 2 0 0 0 20.414 6L18 3.586A2 2 0 0 0 16.586 3H5Zm3 11a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6H8v-6Zm1-7V5h6v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1Z"
clip-rule="evenodd"
/>
<path fill-rule="evenodd" d="M14 17h-4v-2h4v2Z" clip-rule="evenodd" />
</svg>
{render_slot(@inner_block)}
</.success_button>
@@ -147,9 +159,21 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Buttons do
]}
{@rest}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 -ml-0.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-2 -ml-0.5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z">
</path>
<path
fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"
>
</path>
</svg>
{render_slot(@inner_block)}
</.primary_button>
@@ -174,8 +198,22 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Buttons do
]}
{@rest}
>
<svg class="w-6 h-6 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>
<svg
class="w-6 h-6 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
{render_slot(@inner_block)}
</.primary_button>
@@ -0,0 +1,56 @@
defmodule DecentralisedBookIndexWeb.Components.MyComponents.DarkModeSwitcher do
use Phoenix.Component
@doc"""
This component is based on https://github.com/aiwaiwa/phoenix_dark_mode/blob/main/dark_mode.ex.
"""
def dark_mode_switcher(assigns) do
~H"""
<button
id="theme-toggle"
type="button"
phx-update="ignore"
phx-hook="DarkThemeToggle"
class="text-zinc-500 dark:text-zinc-400 hover:bg-slate-200 dark:hover:bg-gray-700 rounded-lg text-sm p-2"
>
<svg
id="theme-toggle-dark-icon"
class="w-5 h-5 text-transparent hidden"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg
id="theme-toggle-light-icon"
class="w-5 h-5 text-transparent"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fill-rule="evenodd"
clip-rule="evenodd"
>
</path>
</svg>
</button>
<script>
// Toggle early based on <html class="dark">
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon != null && themeToggleLightIcon != null) {
let dark = document.documentElement.classList.contains('dark');
const show = dark ? themeToggleLightIcon : themeToggleDarkIcon;
const hide = dark ? themeToggleDarkIcon : themeToggleLightIcon;
show.classList.remove('hidden', 'text-transparent');
hide.classList.add('hidden', 'text-transparent');
}
</script>
"""
end
end
@@ -11,33 +11,56 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.JustSearchResources
def just_search_resources(assigns) do
~H"""
<div class="relative bg-white shadow-md dark:bg-gray-800 sm:rounded-lg">
<div class="relative bg-slate-100 shadow-md dark:bg-gray-800 sm:rounded-lg">
<div class="flex flex-col items-center justify-between p-4 space-y-3 md:flex-row md:space-y-0 md:space-x-4">
<div class="w-full grow">
<form class="flex items-center" phx-change="search" phx-submit="search">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
<svg
aria-hidden="true"
class="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewbox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
</div>
<input name="query" type="text" phx-debounce="300" autocomplete="off" id="simple-search" class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder={@search_placeholder} value={@search_query}>
<input
name="query"
type="text"
phx-debounce="300"
autocomplete="off"
id="simple-search"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder={@search_placeholder}
value={@search_query}
/>
</div>
</form>
</div>
<div class="flex flex-col items-stretch justify-end flex-shrink-0 w-full space-y-2 md:w-auto md:flex-row md:space-y-0 md:items-center md:space-x-3">
<div class="flex items-center w-full space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full">
<select name="sort_by" id="sort_by" class="grow bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %>
<option value={value} selected>{name}</option>
<% else %>
<option value={value}>{name}</option>
<select
name="sort_by"
id="sort_by"
class="grow bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %>
<option value={value} selected>{name}</option>
<% else %>
<option value={value}>{name}</option>
<% end %>
<% end %>
<% end %>
</select>
</select>
</form>
</div>
</div>
@@ -11,48 +11,58 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Pagination do
assigns = assign(assigns, :current_page_number, get_current_page(assigns.page))
~H"""
<nav aria-label="Page navigation example" class="flex justify-center">
<ul class="inline-flex -space-x-px text-base h-10 mx-auto">
<li class="pl-auto">
<.button_link
data-role="previous-page"
kind="primary"
inverse
patch={"#{@endpoint}?#{page_params_which(@page, @params, "prev")}"}
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
disabled={!AshPhoenix.LiveView.prev_page?(@page)}
>
Previous
</.button_link>
</li>
<%= for number <- get_page_numbers(@page) do %>
<%= if number == @current_page_number do %>
<li>
<a href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"}
aria-current="page"
class="flex items-center justify-center px-4 h-10 text-blue-600 border border-gray-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">{number}</a>
</li>
<% else %>
<li>
<a href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"}
class="flex items-center justify-center px-4 h-10 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">{number}</a>
</li>
<%= if AshPhoenix.LiveView.prev_page?(@page) or AshPhoenix.LiveView.next_page?(@page) do %>
<nav aria-label="Page navigation example" class="flex justify-center">
<ul class="inline-flex -space-x-px text-base h-10 mx-auto">
<li class="pl-auto">
<.button_link
data-role="previous-page"
kind="primary"
inverse
patch={"#{@endpoint}?#{page_params_which(@page, @params, "prev")}"}
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight text-gray-500 bg-slate-100 border border-e-0 border-gray-300 rounded-s-lg hover:bg-slate-300 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
disabled={!AshPhoenix.LiveView.prev_page?(@page)}
>
Previous
</.button_link>
</li>
<%= for number <- get_page_numbers(@page) do %>
<%= if number == @current_page_number do %>
<li>
<a
href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"}
aria-current="page"
class="flex items-center justify-center px-4 h-10 text-blue-600 border border-gray-300 bg-blue-100 hover:bg-blue-200 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
>
{number}
</a>
</li>
<% else %>
<li>
<a
href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"}
class="flex items-center justify-center px-4 h-10 leading-tight text-gray-500 bg-slate-100 border border-gray-300 hover:bg-slate-300 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
{number}
</a>
</li>
<% end %>
<% end %>
<% end %>
<li>
<.button_link
data-role="next-page"
kind="primary"
inverse
patch={"#{@endpoint}?#{page_params_which(@page, @params, "next")}"}
class="flex items-center justify-center px-4 h-10 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
disabled={!AshPhoenix.LiveView.next_page?(@page)}
>
Next
</.button_link>
</li>
</ul>
</nav>
<li>
<.button_link
data-role="next-page"
kind="primary"
inverse
patch={"#{@endpoint}?#{page_params_which(@page, @params, "next")}"}
class="flex items-center justify-center px-4 h-10 leading-tight text-gray-500 bg-slate-100 border border-gray-300 rounded-e-lg hover:bg-slate-300 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
disabled={!AshPhoenix.LiveView.next_page?(@page)}
>
Next
</.button_link>
</li>
</ul>
</nav>
<% end %>
"""
end
@@ -17,27 +17,59 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SearchResources do
def search_resources(assigns) do
~H"""
<div class="relative bg-white shadow-md dark:bg-gray-800 sm:rounded-lg">
<div class="relative bg-slate-100 shadow-md dark:bg-gray-800 sm:rounded-lg">
<div class="flex flex-col items-center justify-between p-4 space-y-3 md:flex-row md:space-y-0 md:space-x-4">
<div class="w-full grow">
<form class="flex items-center" phx-change="search" phx-submit="search">
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
<svg
aria-hidden="true"
class="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewbox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
</div>
<input name="query" type="text" phx-debounce="300" autocomplete="off" id="simple-search" class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder={@search_placeholder} value={@search_query}>
<input
name="query"
type="text"
phx-debounce="300"
autocomplete="off"
id="simple-search"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder={@search_placeholder}
value={@search_query}
/>
</div>
</form>
</div>
<div class="flex flex-col items-stretch justify-end flex-shrink-0 w-full space-y-2 md:w-auto md:flex-row md:space-y-0 md:items-center md:space-x-3">
<%= if can_add?(@current_user, @moderator_role) do %>
<.link patch={@resource_new_url}>
<button type="button" class="grow w-full flex items-center justify-left px-4 py-2 text-sm font-medium text-black dark:text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
<svg class="h-3.5 w-3.5 mr-2" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path clip-rule="evenodd" fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
<button
type="button"
class="grow w-full flex items-center justify-left px-4 py-2 text-sm font-medium text-black dark:text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800"
>
<svg
class="h-3.5 w-3.5 mr-2"
fill="currentColor"
viewbox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clip-rule="evenodd"
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
/>
</svg>
Add {@resource_type}
</button>
@@ -45,15 +77,19 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SearchResources do
<% end %>
<div class="flex items-center w-full space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full">
<select name="sort_by" id="sort_by" class="grow bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %>
<option value={value} selected>{name}</option>
<% else %>
<option value={value}>{name}</option>
<select
name="sort_by"
id="sort_by"
class="grow bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %>
<option value={value} selected>{name}</option>
<% else %>
<option value={value}>{name}</option>
<% end %>
<% end %>
<% end %>
</select>
</select>
</form>
</div>
</div>
@@ -64,7 +100,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SearchResources do
defp can_add?(current_user, role) do
case role do
:admin -> current_user != nil and Role.can_moderate?(current_user.role)
:admin -> current_user != nil and Role.can_administrate?(current_user.role)
:moderator -> current_user != nil and Role.can_moderate?(current_user.role)
_ -> false
end
@@ -14,18 +14,54 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectAuthor do
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"></path>
<svg
aria-hidden="true"
class="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
>
</path>
</svg>
</div>
<input name="query" type="text" phx-debounce="300" autocomplete="off" id="simple-search" class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder="Search" value="">
<input
name="query"
type="text"
phx-debounce="300"
autocomplete="off"
id="simple-search"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
value=""
/>
</div>
</form>
<%= for author <- @page.results do %>
<div phx-click="select-author" phx-target={@notify_component} phx-value-author={author.id} phx-value-path={@form_path} class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3">
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">{author.name}</h5>
<%= if Enum.empty?(@page.results) do %>
<div class="flex justify-center ">
<p class="text-lg font-semibold py-5 dark:text-white">
No Authors
</p>
</div>
<% else %>
<%= for author <- @page.results do %>
<div
phx-click="select-author"
phx-target={@notify_component}
phx-value-author={author.id}
phx-value-path={@form_path}
class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{author.name}
</h5>
</div>
<% end %>
<% end %>
</div>
"""
@@ -42,17 +42,25 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectAuthorAlias do
</div>
</form>
<%= for author_alias <- @page.results do %>
<div
phx-click="select-author-alias"
phx-target={@notify_component}
phx-value-author-alias={author_alias.author_alias_registry_id}
class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3"
>
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{author_alias.name}
</h5>
<%= if Enum.empty?(@page.results) do %>
<div class="flex justify-center ">
<p class="text-lg font-semibold py-5 dark:text-white">
No Authors
</p>
</div>
<% else %>
<%= for author_alias <- @page.results do %>
<div
phx-click="select-author-alias"
phx-target={@notify_component}
phx-value-author-alias={author_alias.author_alias_registry_id}
class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{author_alias.name}
</h5>
</div>
<% end %>
<% end %>
</div>
"""
@@ -42,17 +42,25 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectBookEdition do
</div>
</form>
<%= for book_edition <- @page.results do %>
<div
phx-click="select-book-edition"
phx-target={@notify_component}
phx-value-book-edition={book_edition.book_editions_registry_id}
class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3"
>
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{book_edition.title}
</h5>
<%= if Enum.empty?(@page.results) do %>
<div class="flex justify-center ">
<p class="text-lg font-semibold py-5 dark:text-white">
No Books
</p>
</div>
<% else %>
<%= for book_edition <- @page.results do %>
<div
phx-click="select-book-edition"
phx-target={@notify_component}
phx-value-book-edition={book_edition.book_editions_registry_id}
class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{book_edition.title}
</h5>
</div>
<% end %>
<% end %>
</div>
"""
@@ -14,18 +14,53 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectPublisher do
<label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"></path>
<svg
aria-hidden="true"
class="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
>
</path>
</svg>
</div>
<input name="query" type="text" phx-debounce="300" autocomplete="off" id="simple-search" class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder="Search" value="">
<input
name="query"
type="text"
phx-debounce="300"
autocomplete="off"
id="simple-search"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Search"
value=""
/>
</div>
</form>
<%= for publisher <- @page.results do %>
<div phx-click="select-publisher" phx-target={@notify_component} phx-value-publisher={publisher.id} class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3">
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">{publisher.name}</h5>
<%= if Enum.empty?(@page.results) do %>
<div class="flex justify-center ">
<p class="text-lg font-semibold py-5 dark:text-white">
No Publishers
</p>
</div>
<% else %>
<%= for publisher <- @page.results do %>
<div
phx-click="select-publisher"
phx-target={@notify_component}
phx-value-publisher={publisher.id}
class="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 mx-auto my-3 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{publisher.name}
</h5>
</div>
<% end %>
<% end %>
</div>
"""
@@ -20,8 +20,6 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectedAuthor do
errors =
if Phoenix.Component.used_input?(assigns.author_form), do: assigns.author_form.errors, else: []
IO.inspect("errors")
IO.inspect(errors)
assigns =
assigns
@@ -9,6 +9,9 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Footer do
<span class="block text-sm text-gray-500 text-center dark:text-gray-400">
© 2025 Kostiantyn Klochko. All Rights Reserved.
</span>
<span class="block text-sm text-gray-500 text-center dark:text-gray-400">
<a href="https://git.kklochko.space/KKlochko/decentralised_book_index">Decentralised book index</a> is free software: you can redistribute it and/or modify it under the terms of <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">the GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
</span>
</div>
</footer>
"""
@@ -2,7 +2,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
use Phoenix.Component
use DecentralisedBookIndexWeb, :verified_routes
import DecentralisedBookIndexWeb.Components.MyComponents.Buttons
import DecentralisedBookIndexWeb.Components.MyComponents.{Buttons, DarkModeSwitcher}
alias Phoenix.LiveView.JS
alias DecentralisedBookIndex.Accounts.Role
@@ -11,7 +12,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
def partial_navbar(assigns) do
~H"""
<nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
<nav class="bg-slate-100 border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
<div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
<.link patch={~p"/books"} class="flex items-center">
<svg
@@ -35,6 +36,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">DBI</span>
</.link>
<div class="flex items-center lg:order-2">
<.dark_mode_switcher />
<.user_info current_user={@current_user} />
<button
data-collapse-toggle="mobile-menu-2"
@@ -106,7 +108,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
</a>
</li>
<% end %>
<%= if @current_user != nil and Role.can_administrate?(@current_user.role) do %>
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<li>
<a
href={~p"/servers/"}
@@ -153,7 +155,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
<!-- Dropdown menu -->
<div
id="dropdown-user-info"
class="z-10 hidden text-base list-none bg-white rounded-lg shadow-sm w-40 dark:bg-gray-700 flex flex-col items-center pt-2 border dark:border-gray-600"
class="z-10 hidden text-base list-none bg-slate-100 rounded-lg shadow-sm w-40 dark:bg-gray-700 flex flex-col items-center pt-2 border dark:border-gray-600"
>
<%= if @current_user.role != :user do %>
<p class="text-zinc-700 dark:text-white">
@@ -158,9 +158,6 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.FormComponent do
end
def handle_event("select-author-alias", %{"author-alias" => author_alias_id}, socket) do
IO.inspect("book_editions_registry_id")
IO.inspect(author_alias_id)
socket =
socket
|> update(:form, fn form ->
@@ -30,19 +30,11 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Index do
<% else %>
<div class="flex flex-wrap flex-[3_1_auto]">
<%= for author <- @page.results do %>
<.author_card
author={author}
current_user={@current_user}
/>
<.author_card author={author} current_user={@current_user} />
<% end %>
</div>
<.pagination
endpoint={~p"/authors"}
page={@page}
page_params={@page_params}
params={@params}
/>
<.pagination endpoint={~p"/authors"} page={@page} page_params={@page_params} params={@params} />
<% end %>
"""
end
@@ -2,6 +2,7 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
use DecentralisedBookIndexWeb, :live_view
alias DecentralisedBookIndex.Accounts.Role
alias DecentralisedBookIndex.Metadata
@impl true
def render(assigns) do
@@ -27,7 +28,7 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
</:actions>
</.header>
<div class="flex flex-col items-start gap-6 max-w-6xl mx-auto p-4 bg-gray-900 rounded-lg transition-all duration-300 md:prose md:block">
<div class="flex flex-col items-start gap-6 max-w-6xl mx-auto p-4 rounded-lg transition-all duration-300 md:prose md:block">
<%= if @author.avatar_url != nil do %>
<img
class="w-36 h-36 mb-3 rounded-full shadow-lg mx-auto md:float-left md:mr-4"
@@ -54,9 +55,28 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
<div class="flex-1 pt-2 lg:pt-0 min-h-36">
<h2 class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Description</h2>
<p class="text-gray-300 leading-relaxed">
<p class="text-zinc-800 dark:text-gray-400 leading-relaxed">
{@author.description}
</p>
<%= if not is_nil(@author.dbi_server) do %>
<dl>
<dt class="mt-4 mb-2 font-semibold leading-none text-gray-900 dark:text-white">
From Server
</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<.link navigate={~p"/servers/#{@author.dbi_server.id}/"} class="hover:underline">
{@author.dbi_server.name}
</.link>
<% else %>
<.link navigate={@author.dbi_server.url} class="hover:underline">
{@author.dbi_server.name}
</.link>
<% end %>
</dd>
</dl>
<% end %>
</div>
</div>
@@ -77,6 +97,19 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
</ul>
<% end %>
<%= if not Enum.empty?(@page.results) do %>
<h2 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white mt-10">
Books
</h2>
<div class="flex flex-wrap flex-[3_1_auto]">
<%= for book <- @page.results do %>
<.book_card book={book} current_user={@current_user} />
<% end %>
</div>
<.pagination endpoint={~p"/authors/#{@author}"} page={@page} page_params={@page_params} params={@params} />
<% end %>
<.back navigate={~p"/authors"}>Back to authors</.back>
"""
end
@@ -87,16 +120,30 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
def handle_params(%{"id" => id} = params, _, socket) do
author =
Ash.get!(DecentralisedBookIndex.Metadata.Author, id, load: [:dbi_server], actor: socket.assigns.current_user)
Ash.get!(Metadata.Author, id,
load: [:dbi_server],
actor: socket.assigns.current_user
)
alternative_names = DecentralisedBookIndex.Metadata.get_author_alternative_names!(author)
alternative_names = Metadata.get_author_alternative_names!(author)
page_params = AshPhoenix.LiveView.page_from_params(params, 10)
page =
Metadata.get_author_books!(author,
load: [:brief_description],
page: page_params ++ [count: true],
actor: socket.assigns.current_user
)
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:author, author)
|> assign(:page_params, page_params)
|> assign(:page, page)
|> assign(:params, params)
|> assign(:alternative_names, alternative_names)}
end
@@ -21,19 +21,11 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
<%= if @form.source.type == :create do %>
<.input field={@form[:title]} type="text" label="Title" />
<.input field={@form[:cover_image_url]} type="text" label="Cover image url" />
<.input
field={@form[:description]}
type="textarea"
label="Description"
/>
<.input field={@form[:description]} type="textarea" label="Description" />
<.bids_inputs form={@form} myself={@myself} />
<.author_roles_inputs form={@form} myself={@myself} notify_component={@myself} />
<.input field={@form[:format]} type="text" label="Format" />
<.input
field={@form[:language]}
type="text"
label="Language"
/>
<.input field={@form[:language]} type="text" label="Language" />
<.input field={@form[:page_count]} type="number" label="Page count" />
<div>
<.input field={@form[:publisher_id]} type="text" label="Publisher" type="hidden" show_errors?={false} />
@@ -48,19 +40,11 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
<%= if @form.source.type == :update do %>
<.input field={@form[:title]} type="text" label="Title" />
<.input field={@form[:cover_image_url]} type="text" label="Cover image url" />
<.input
field={@form[:description]}
type="textarea"
label="Description"
/>
<.input field={@form[:description]} type="textarea" label="Description" />
<.bids_inputs form={@form} myself={@myself} />
<.author_roles_inputs form={@form} myself={@myself} notify_component={@myself} />
<.input field={@form[:format]} type="text" label="Format" />
<.input
field={@form[:language]}
type="text"
label="Language"
/>
<.input field={@form[:language]} type="text" label="Language" />
<.input field={@form[:page_count]} type="number" label="Page count" />
<div>
<.input field={@form[:publisher_id]} type="text" label="Publisher" type="hidden" show_errors?={false} />
@@ -145,16 +129,30 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
<tbody phx-hook="bidSort" id="bidSort" phx-target={@myself}>
<.inputs_for :let={bid_form} field={@form[:bids]}>
<tr data-id={bid_form.index}>
<td class="px-3 w-10 pt-2">
<svg class="w-6 h-6 text-gray-800 dark:text-white handle cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4"/>
<td class="px-3 w-10 h-10 pt-2 align-top group">
<svg
class="w-6 h-6 text-gray-800 dark:text-white handle cursor-pointer mt-2"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m8 15 4 4 4-4m0-6-4-4-4 4"
/>
</svg>
</td>
<td class="px-3 w-36">
<td class="px-3 w-36 align-top group">
<label for={bid_form[:type].id} class="hidden">Type</label>
<.input field={bid_form[:type]} />
</td>
<td class="px-3">
<td class="px-3 align-top group">
<label for={bid_form[:bid].id} class="hidden">Id</label>
<.input field={bid_form[:bid]} />
</td>
@@ -167,8 +165,20 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
size="xs"
inverse
>
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/>
<svg
class="w-6 h-6 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z"
clip-rule="evenodd"
/>
</svg>
</.button_link>
</td>
@@ -229,12 +239,26 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
<tbody phx-hook="authorRoleSort" id="authorRoleSort" phx-target={@myself}>
<.inputs_for :let={author_roles_form} field={@form[:author_roles]}>
<tr data-id={author_roles_form.index}>
<td class="px-3 w-10 pt-2">
<svg class="w-6 h-6 text-gray-800 dark:text-white handle cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4"/>
<td class="px-3 w-10 pt-2 align-top group">
<svg
class="w-6 h-6 text-gray-800 dark:text-white handle cursor-pointer mt-2"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m8 15 4 4 4-4m0-6-4-4-4 4"
/>
</svg>
</td>
<td class="px-3 w-48">
<td class="px-3 w-48 align-top group">
<label for={author_roles_form[:author_id].id} class="hidden">Type</label>
<.input field={author_roles_form[:author_id]} type="hidden" show_errors?={false} />
<.selected_author
@@ -244,7 +268,7 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
class="mt-2 w-full"
/>
</td>
<td class="px-3">
<td class="px-3 align-top group">
<label for={author_roles_form[:role].id} class="hidden">Id</label>
<.input field={author_roles_form[:role]} />
</td>
@@ -257,8 +281,20 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
size="xs"
inverse
>
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/>
<svg
class="w-6 h-6 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z"
clip-rule="evenodd"
/>
</svg>
</.button_link>
</td>
@@ -30,19 +30,11 @@ defmodule DecentralisedBookIndexWeb.BookLive.Index do
<% else %>
<div class="flex flex-wrap flex-[3_1_auto]">
<%= for book <- @page.results do %>
<.book_card
book={book}
current_user={@current_user}
/>
<.book_card book={book} current_user={@current_user} />
<% end %>
</div>
<.pagination
endpoint={~p"/books"}
page={@page}
page_params={@page_params}
params={@params}
/>
<.pagination endpoint={~p"/books"} page={@page} page_params={@page_params} params={@params} />
<% end %>
<.modal :if={@live_action in [:new, :edit]} id="book-modal" show on_cancel={JS.patch(~p"/books")}>
@@ -33,11 +33,27 @@ defmodule DecentralisedBookIndexWeb.BookLive.Show do
<div class="mb-4">
<%= if @book.cover_image_url != nil do %>
<img class="w-36 shadow-lg mt-14 mx-auto object-cover" src={@book.cover_image_url} alt={"#{@book.title} image"}/>
<img
class="w-36 shadow-lg mt-14 mx-auto object-cover"
src={@book.cover_image_url}
alt={"#{@book.title} image"}
/>
<% else %>
<div class="relative w-36 h-36 overflow-hidden mt-14 mx-auto">
<svg class="w-36 h-36 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M6 2a2 2 0 0 0-2 2v15a3 3 0 0 0 3 3h12a1 1 0 1 0 0-2h-2v-2h2a1 1 0 0 0 1-1V4a2 2 0 0 0-2-2h-8v16h5v2H7a1 1 0 1 1 0-2h1V2H6Z" clip-rule="evenodd"/>
<svg
class="w-36 h-36 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M6 2a2 2 0 0 0-2 2v15a3 3 0 0 0 3 3h12a1 1 0 1 0 0-2h-2v-2h2a1 1 0 0 0 1-1V4a2 2 0 0 0-2-2h-8v16h5v2H7a1 1 0 1 1 0-2h1V2H6Z"
clip-rule="evenodd"
/>
</svg>
</div>
<% end %>
@@ -48,10 +64,14 @@ defmodule DecentralisedBookIndexWeb.BookLive.Show do
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Description</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@book.description}</dd>
</dl>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Publisher</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@book.publisher.name}</dd>
</dl>
<%= if not is_nil(@book.publisher) do %>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Publisher</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{@book.publisher.name}
</dd>
</dl>
<% end %>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Published</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@book.published}</dd>
@@ -68,22 +88,39 @@ defmodule DecentralisedBookIndexWeb.BookLive.Show do
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Language</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@book.language}</dd>
</dl>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Identifiers</dt>
<%= for bid <- @book.bids do %>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{bid.type}: {bid.bid}</dd>
<% end %>
</dl>
<%= if not Enum.empty?(@book.bids) do %>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Identifiers</dt>
<%= for bid <- @book.bids do %>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{bid.type}: {bid.bid}
</dd>
<% end %>
</dl>
<% end %>
<%= if not is_nil(@book.dbi_server) do %>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">From Server</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<.link navigate={~p"/servers/#{@book.dbi_server.id}/"} class="hover:underline">
{@book.dbi_server.name}
</.link>
<% else %>
<.link navigate={@book.dbi_server.url} class="hover:underline">
{@book.dbi_server.name}
</.link>
<% end %>
</dd>
</dl>
<% end %>
</div>
<%= if not Enum.empty?(@alternative_editions) do %>
<h1 class="text-lg font-semibold leading-8 text-zinc-800 dark:text-white">Editions</h1>
<div class="flex flex-wrap flex-[3_1_auto]">
<%= for book <- @alternative_editions do %>
<.book_card
book={book}
current_user={@current_user}
/>
<.book_card book={book} current_user={@current_user} />
<% end %>
</div>
<% end %>
@@ -20,7 +20,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.FormComponent do
<.input field={@form[:name]} type="text" label="Name" />
<.input field={@form[:url]} type="text" label="Url" />
<% end %>
<%= if @form.source.type == :update and is_nil(@form[:dbi_server].value) do %>
<%= if @form.source.type == :update and is_nil(@form[:dbi_server_id].value) do %>
<.input field={@form[:name]} type="text" label="Name" />
<.input field={@form[:url]} type="text" label="Url" />
<% end %>
@@ -61,7 +61,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.FormComponent do
socket =
socket
|> put_flash(:info, "Server #{socket.assigns.form.source.type}d successfully")
|> push_navigate(to: patch_url(socket.assigns.action, dbi_server.id))
|> redirect(to: patch_url(socket.assigns.action, dbi_server.id))
{:noreply, socket}
@@ -4,26 +4,30 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.SyncTasks.SyncServerTask
alias DecentralisedBookIndex.Accounts.Role
@impl true
def render(assigns) do
~H"""
<.header>
Listing Servers
<:actions>
<div class="flex flex-row gap-2">
<%= if @current_user != nil and Role.can_administrate?(@current_user.role) do %>
<div class="flex flex-row gap-2">
<.primary_button phx-click="sync">
Sync now
</.primary_button>
</div>
<div class="flex flex-row gap-2">
<.link patch={~p"/oban"}>
<.primary_button>
Task dashboard
<div class="flex flex-row gap-2">
<.primary_button phx-click="sync">
Sync now
</.primary_button>
</.link>
</div>
<div class="flex flex-row gap-2">
<.link patch={~p"/oban"}>
<.primary_button>
Task dashboard
</.primary_button>
</.link>
</div>
</div>
</div>
<% end %>
</:actions>
</.header>
@@ -63,16 +67,13 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
<.link navigate={~p"/servers/#{dbi_server}"}>Show</.link>
</div>
<.link patch={~p"/servers/#{dbi_server}/edit"}>Edit</.link>
<%= if @current_user != nil and Role.can_administrate?(@current_user.role) do %>
<.link patch={~p"/servers/#{dbi_server}/edit"}>Edit</.link>
<% end %>
</:action>
</.table>
<.pagination
endpoint={~p"/servers"}
page={@page}
page_params={@page_params}
params={@params}
/>
<.pagination endpoint={~p"/servers"} page={@page} page_params={@page_params} params={@params} />
</div>
<% end %>
"""
@@ -81,7 +82,10 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(DecentralisedBookIndex.PubSub, DecentralisedBookIndex.SyncWorkerManual.topic())
Phoenix.PubSub.subscribe(
DecentralisedBookIndex.PubSub,
DecentralisedBookIndex.SyncWorkerManual.topic()
)
end
{:ok,
@@ -1,6 +1,8 @@
defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
use DecentralisedBookIndexWeb, :live_view
alias DecentralisedBookIndex.Accounts.Role
@impl true
def render(assigns) do
~H"""
@@ -8,7 +10,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
{@dbi_server.name}
<:actions>
<%= if is_nil(@dbi_server.dbi_server) do %>
<%= if @current_user != nil and Role.can_administrate?(@current_user.role) do %>
<.link patch={~p"/servers/#{@dbi_server}/edit"} phx-click={JS.push_focus()}>
<.edit_button>
Edit
@@ -18,7 +20,6 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
</:actions>
</.header>
<div>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Url</dt>
@@ -26,8 +27,20 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
</dl>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Sync on?</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@dbi_server.sync_on?}</dd>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{@dbi_server.sync_on?}
</dd>
</dl>
<%= if not is_nil(@dbi_server.dbi_server) do %>
<dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">From Server</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
<.link navigate={~p"/servers/#{@dbi_server.dbi_server.id}/"} class="hover:underline">
{@dbi_server.dbi_server.name}
</.link>
</dd>
</dl>
<% end %>
</div>
<.back navigate={~p"/servers"}>Back to servers</.back>
@@ -46,7 +59,10 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(
:dbi_server,
Ash.get!(DecentralisedBookIndex.Metadata.DBIServer, id, load: [:dbi_server], actor: socket.assigns.current_user)
Ash.get!(DecentralisedBookIndex.Metadata.DBIServer, id,
load: [:dbi_server],
actor: socket.assigns.current_user
)
)}
end
@@ -1,6 +1,8 @@
defmodule DecentralisedBookIndexWeb.PublisherLive.Show do
use DecentralisedBookIndexWeb, :live_view
alias DecentralisedBookIndex.Accounts.Role
@impl true
def render(assigns) do
~H"""
@@ -18,6 +20,27 @@ defmodule DecentralisedBookIndexWeb.PublisherLive.Show do
</:actions>
</.header>
<%= if not is_nil(@publisher.dbi_server) do %>
<div>
<dl>
<dt class="mt-4 mb-2 font-semibold leading-none text-gray-900 dark:text-white">
From Server
</dt>
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<.link navigate={~p"/servers/#{@publisher.dbi_server.id}/"} class="hover:underline">
{@publisher.dbi_server.name}
</.link>
<% else %>
<.link navigate={@publisher.dbi_server.url} class="hover:underline">
{@publisher.dbi_server.name}
</.link>
<% end %>
</dd>
</dl>
</div>
<% end %>
<.back navigate={~p"/publishers"}>Back to publishers</.back>
"""
end
@@ -34,7 +57,10 @@ defmodule DecentralisedBookIndexWeb.PublisherLive.Show do
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(
:publisher,
Ash.get!(DecentralisedBookIndex.Metadata.Publisher, id, load: [:dbi_server], actor: socket.assigns.current_user)
Ash.get!(DecentralisedBookIndex.Metadata.Publisher, id,
load: [:dbi_server],
actor: socket.assigns.current_user
)
)}
end
@@ -30,11 +30,7 @@ defmodule DecentralisedBookIndexWeb.UserLive.Index do
</div>
<% else %>
<div class="pt-2 flex flex-col gap-2">
<.table
id="users"
rows={@users}
row_click={fn user -> JS.navigate(~p"/users/#{user}") end}
>
<.table id="users" rows={@users} row_click={fn user -> JS.navigate(~p"/users/#{user}") end}>
<:col :let={user} label="Email">{user.email}</:col>
<:col :let={user} label="Role">{user.role}</:col>
@@ -48,12 +44,7 @@ defmodule DecentralisedBookIndexWeb.UserLive.Index do
</:action>
</.table>
<.pagination
endpoint={~p"/servers"}
page={@page}
page_params={@page_params}
params={@params}
/>
<.pagination endpoint={~p"/servers"} page={@page} page_params={@page_params} params={@params} />
</div>
<% end %>
"""
@@ -13,7 +13,7 @@ defmodule DecentralisedBookIndexWeb.Plugs.RequireAdmin do
conn
else
conn
|> put_flash(:error, "Unauthorized")
|> put_flash(:error, "Unauthorized!")
|> redirect(to: "/")
|> halt()
end
+12 -10
View File
@@ -41,6 +41,18 @@ defmodule DecentralisedBookIndexWeb.Router do
scope "/", DecentralisedBookIndexWeb do
pipe_through :browser
ash_authentication_live_session :admin_authenticated_routes,
on_mount: {DecentralisedBookIndexWeb.LiveUserAuth, :admin_required} do
live "/servers/new", DbiServerLive.Edit, :new
live "/servers/:id/edit", DbiServerLive.Edit, :edit
#live "/servers", DbiServerLive.Index, :index
#live "/servers/:id", DbiServerLive.Show, :show
live "/users", UserLive.Index, :index
live "/users/:id", UserLive.Show, :show
live "/users/:id/edit", UserLive.Edit, :edit
end
ash_authentication_live_session :moderator_authenticated_routes,
on_mount: {DecentralisedBookIndexWeb.LiveUserAuth, :moderator_required} do
live "/books/new", BookLive.Edit, :new
@@ -56,19 +68,9 @@ defmodule DecentralisedBookIndexWeb.Router do
live "/publishers/:id/edit", PublisherLive.Edit, :edit
live "/publishers/:id", PublisherLive.Show, :show
end
ash_authentication_live_session :admin_authenticated_routes,
on_mount: {DecentralisedBookIndexWeb.LiveUserAuth, :admin_required} do
live "/servers", DbiServerLive.Index, :index
live "/servers/new", DbiServerLive.Edit, :new
live "/servers/:id/edit", DbiServerLive.Edit, :edit
live "/servers/:id", DbiServerLive.Show, :show
live "/users", UserLive.Index, :index
live "/users/:id", UserLive.Show, :show
live "/users/:id/edit", UserLive.Edit, :edit
end
ash_authentication_live_session :maybe_authenticated_routes,
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule DecentralisedBookIndex.MixProject do
def project do
[
app: :decentralised_book_index,
version: "0.1.0",
version: "1.1.0",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@@ -0,0 +1,21 @@
defmodule DecentralisedBookIndex.Repo.Migrations.UpdateToAllowABookToHaveNoPublisher do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:books) do
modify :publisher_id, :uuid, null: true
end
end
def down do
alter table(:books) do
modify :publisher_id, :uuid, null: false
end
end
end
@@ -0,0 +1,227 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "title",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "published",
"type": "date"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "language",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "format",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "page_count",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "cover_image_url",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "books_dbi_server_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "dbi_servers"
},
"size": null,
"source": "dbi_server_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "books_book_editions_registry_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "book_editions_registries"
},
"size": null,
"source": "book_editions_registry_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "books_publisher_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "publishers"
},
"size": null,
"source": "publisher_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [
{
"all_tenants?": false,
"concurrently": false,
"error_fields": [],
"fields": [
{
"type": "string",
"value": "title gin_trgm_ops"
}
],
"include": null,
"message": null,
"name": "book_title_gin_index",
"nulls_distinct": true,
"prefix": null,
"table": null,
"unique": false,
"using": "GIN",
"where": null
}
],
"custom_statements": [],
"has_create_action": true,
"hash": "AA67DC03E5D78606517F268BD28B58521F2000DE3BA531FA48363CCD08CE4475",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.DecentralisedBookIndex.Repo",
"schema": null,
"table": "books"
}
@@ -56,7 +56,7 @@ defmodule DecentralisedBookIndex.Metadata.AuthorTest do
test "new author has no alternatives names", %{user: user} do
{:ok, author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
assert {:ok, alternatives_names} = Metadata.get_author_alternative_names(author)
assert alternatives_names = []
assert ^alternatives_names = []
end
test "author has related author so they has one alternative name", %{user: user} do
@@ -73,7 +73,7 @@ defmodule DecentralisedBookIndex.Metadata.AuthorTest do
)
assert {:ok, alternatives_names} = Metadata.get_author_alternative_names(author)
assert alternatives_names = [related_author]
assert get_ids(alternatives_names) == [related_author.id]
end
end
@@ -118,7 +118,8 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
bids,
author_roles,
publisher.id,
nil
nil,
actor: user
)
end
end
@@ -146,7 +147,7 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
)
assert {:ok, alternatives_names} = Metadata.get_book_alternative_editions(book)
assert alternatives_names = []
assert ^alternatives_names = []
end
test "book has related book so they has one alternative name", %{user: user} do
@@ -187,7 +188,7 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
)
assert {:ok, alternatives_names} = Metadata.get_book_alternative_editions(book)
assert alternatives_names = [related_book]
assert get_ids(alternatives_names) == [related_book.id]
end
end
@@ -218,6 +219,9 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
)
assert {:ok, books} = Metadata.get_author_books(author)
book_ids = Enum.map(books.results, & &1.id)
assert book.id in book_ids
end
test "get the list contains aliases' books", %{user: user} do
@@ -247,7 +251,7 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
assert {:ok, books} = Metadata.get_author_books(author)
book_ids = Enum.map(books, & &1.id)
book_ids = Enum.map(books.results, & &1.id)
assert book.id in book_ids
assert book2.id in book_ids
@@ -256,7 +260,7 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
end
describe "get a book by book id" do
test "get by bid", %{user: user} do
test "get by bid" do
expected_book = generate(book())
[%Metadata.BookId{type: type, bid: bid} | _] = expected_book.bids
@@ -12,13 +12,8 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
test "user can't submit" do
user = generate(user())
valid_params = %{
"name" => "Oleh",
"description" => "A cool author"
}
assert_raise Ash.Error.Forbidden, fn ->
form =
_form =
AshPhoenix.Form.for_create(Metadata.Author, :create,
as: "author",
actor: user
@@ -28,12 +23,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
end
test "moderator can submit", %{user: user} do
valid_params = %{
"name" => "Oleh",
"description" => "A cool author"
}
assert form =
assert _form =
AshPhoenix.Form.for_create(Metadata.Author, :create,
as: "author",
actor: user
@@ -55,7 +45,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
actor: user
)
assert {:ok, author} = AshPhoenix.Form.submit(form, params: valid_params)
assert {:ok, _author} = AshPhoenix.Form.submit(form, params: valid_params)
end
end
@@ -74,7 +64,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
actor: user
)
assert {:ok, author} = AshPhoenix.Form.submit(form, params: valid_params)
assert {:ok, _author} = AshPhoenix.Form.submit(form, params: valid_params)
end
end
end
@@ -4,7 +4,6 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
import ExUnitProperties
alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.Metadata.Book
setup do
user = generate(user(role: :moderator))
@@ -14,22 +13,9 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
describe "form submiting" do
test "user can't submit" do
user = generate(user())
bids = bids()
author_roles = author_roles()
valid_params = %{
"title" => "Book",
"description" => "A cool author",
"format" => "Paper",
"language" => "English",
"page_count" => 600,
"published" => ~D[2025-03-06],
"author_roles" => author_roles,
"bids" => bids
}
assert_raise Ash.Error.Forbidden, fn ->
form =
_form =
AshPhoenix.Form.for_create(Metadata.Book, :create,
as: "book",
actor: user
@@ -39,21 +25,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
end
test "moderator can submit", %{user: user} do
bids = bids(actor: user)
author_roles = author_roles(actor: user)
valid_params = %{
"title" => "Book",
"description" => "A cool author",
"format" => "Paper",
"language" => "English",
"page_count" => 600,
"published" => ~D[2025-03-06],
"author_roles" => author_roles,
"bids" => bids
}
assert form =
assert _form =
AshPhoenix.Form.for_create(Metadata.Book, :create,
as: "book",
actor: user
@@ -86,7 +58,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
actor: user
)
assert {:ok, user} = AshPhoenix.Form.submit(form, params: valid_params)
assert {:ok, _book} = AshPhoenix.Form.submit(form, params: valid_params)
end
end
@@ -106,7 +78,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
actor: user
)
assert {:ok, user} = AshPhoenix.Form.submit(form, params: valid_params)
assert {:ok, _book} = AshPhoenix.Form.submit(form, params: valid_params)
end
test "update bids and authors", %{user: user} do
@@ -125,7 +97,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
actor: user
)
assert {:ok, user} = AshPhoenix.Form.submit(form, params: valid_params)
assert {:ok, _book} = AshPhoenix.Form.submit(form, params: valid_params)
end
end
@@ -12,12 +12,8 @@ defmodule DecentralisedBookIndex.Metadata.Forms.PublisherFormTest do
test "user can't submit" do
user = generate(user())
valid_params = %{
"name" => "Publisher"
}
assert_raise Ash.Error.Forbidden, fn ->
form =
_form =
AshPhoenix.Form.for_create(Metadata.Publisher, :create,
as: "publisher",
actor: user
@@ -27,11 +23,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.PublisherFormTest do
end
test "moderator can submit", %{user: user} do
valid_params = %{
"name" => "Publisher"
}
assert form =
assert _form =
AshPhoenix.Form.for_create(Metadata.Publisher, :create,
as: "publisher",
actor: user
@@ -81,5 +81,40 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorTransformerTest do
updated_at: "2025-03-26T10:16:16.124581Z"
} = author
end
test "a json has dbi_server_id as nil, then author's map has not :dbi_server_id key" do
json_body = %{
"attributes" => %{
"author_alias_registry_id" => "1320a6fe-b311-45ac-bbaa-39cf29c44624",
"avatar_url" => "/images/avatar.png",
"dbi_server_id" => nil,
"description" => "Something",
"inserted_at" => "2025-03-25T19:19:15.187456Z",
"name" => "Author",
"updated_at" => "2025-03-26T10:16:16.124581Z"
},
"id" => "889a323e-d104-4b5d-b276-dad5a9b1da9d",
"links" => %{},
"meta" => %{},
"relationships" => %{
"author_alias_registry" => %{"links" => %{}, "meta" => %{}},
"dbi_server" => %{"links" => %{}, "meta" => %{}}
},
"type" => "author"
}
assert {:ok, author} = AuthorTransformer.from_json(json_body)
assert %{
id: "889a323e-d104-4b5d-b276-dad5a9b1da9d",
name: "Author",
description: "Something",
avatar_url: "/images/avatar.png",
author_alias_registry_id: "1320a6fe-b311-45ac-bbaa-39cf29c44624",
inserted_at: "2025-03-25T19:19:15.187456Z",
updated_at: "2025-03-26T10:16:16.124581Z"
} = author
refute Map.has_key?(author, :dbi_server_id)
end
end
end
@@ -138,5 +138,70 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookTransformerTest do
dbi_server_id: "889a323e-d104-4b5d-b276-dad5a9b1da99"
} = book
end
test "a json has dbi_server_id as nil, then book's map has not :dbi_server_id key" do
json_body = %{
"attributes" => %{
"dbi_server_id" => nil,
"cover_image_url" => "/images/book_cover.png",
"description" => "A cool book.",
"format" => "Paper",
"inserted_at" => "2025-03-20T14:44:36.162986Z",
"language" => "English",
"page_count" => 1000,
"published" => "2025-03-05",
"publisher_id" => "11349865-1b7b-454a-b999-6c4059888a78",
"title" => "Book",
"updated_at" => "2025-04-01T18:14:25.754055Z"
},
"id" => "1bbe8861-9d9d-4684-bda6-b6ec238d8d08",
"links" => %{},
"meta" => %{},
"relationships" => %{
"author_roles" => %{
"links" => %{
"related" =>
"http://localhost:4000/api/v1/json/books/1bbe8861-9d9d-4684-bda6-b6ec238d8d08/author_roles"
},
"meta" => %{}
},
"bids" => %{
"links" => %{
"related" =>
"http://localhost:4000/api/v1/json/books/1bbe8861-9d9d-4684-bda6-b6ec238d8d08/bids"
},
"meta" => %{}
},
"book_editions_registry" => %{"links" => %{}, "meta" => %{}},
"dbi_server" => %{"links" => %{}, "meta" => %{}},
"publisher" => %{
"links" => %{
"related" =>
"http://localhost:4000/api/v1/json/books/1bbe8861-9d9d-4684-bda6-b6ec238d8d08/publisher"
},
"meta" => %{}
}
},
"type" => "book"
}
assert {:ok, book} = BookTransformer.from_json(json_body)
assert %{
id: "1bbe8861-9d9d-4684-bda6-b6ec238d8d08",
cover_image_url: "/images/book_cover.png",
description: "A cool book.",
format: "Paper",
inserted_at: "2025-03-20T14:44:36.162986Z",
language: "English",
page_count: 1000,
published: "2025-03-05",
publisher_id: "11349865-1b7b-454a-b999-6c4059888a78",
title: "Book",
updated_at: "2025-04-01T18:14:25.754055Z",
} = book
refute Map.has_key?(book, :dbi_server_id)
end
end
end
@@ -8,6 +8,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
json_body = %{
"data" => %{
"attributes" => %{
"dbi_server_id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
"inserted_at" => "2025-03-22T20:07:30.766249Z",
"name" => "Test",
"updated_at" => "2025-05-04T18:48:44.213309Z",
@@ -16,7 +17,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
"id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
"links" => %{},
"meta" => %{},
"relationships" => %{},
"relationships" => %{"dbi_server" => %{"links" => %{}, "meta" => %{}}},
"type" => "dbi_server"
},
"jsonapi" => %{"version" => "1.0"},
@@ -34,11 +35,40 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
name: "Test",
url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z"
updated_at: "2025-05-04T18:48:44.213309Z",
dbi_server_id: "0c0647ec-07ef-4caa-b683-5847dbfbe5cc"
} = server
end
test "a json doesn't contains server information \"data\" attribute" do
json_body = %{
"attributes" => %{
"dbi_server_id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
"inserted_at" => "2025-03-22T20:07:30.766249Z",
"name" => "Test",
"updated_at" => "2025-05-04T18:48:44.213309Z",
"url" => "http://localhost:4001"
},
"id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
"links" => %{},
"meta" => %{},
"relationships" => %{"dbi_server" => %{"links" => %{}, "meta" => %{}}},
"type" => "dbi_server"
}
assert {:ok, server} = DBIServerTransformer.from_json(json_body)
assert %{
id: "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
name: "Test",
url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z",
dbi_server_id: "0c0647ec-07ef-4caa-b683-5847dbfbe5cc"
} = server
end
test "a json has dbi_server_id as nil, then server's map has not :dbi_server_id key" do
json_body = %{
"attributes" => %{
"inserted_at" => "2025-03-22T20:07:30.766249Z",
@@ -62,6 +92,8 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z"
} = server
refute Map.has_key?(server, :dbi_server_id)
end
end
end
@@ -63,5 +63,32 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.PublisherTransformerTest
dbi_server_id: "889a323e-d104-4b5d-b276-dad5a9b1da99"
} = publisher
end
test "a json has dbi_server_id as nil, then publisher's map has not :dbi_server_id key" do
json_body = %{
"attributes" => %{
"dbi_server_id" => nil,
"inserted_at" => "2025-03-21T09:20:48.791539Z",
"name" => "Publisher",
"updated_at" => "2025-03-21T09:20:48.791539Z"
},
"id" => "11349865-1b7b-454a-b999-6c4059888a78",
"links" => %{},
"meta" => %{},
"relationships" => %{"dbi_server" => %{"links" => %{}, "meta" => %{}}},
"type" => "publisher"
}
assert {:ok, publisher} = PublisherTransformer.from_json(json_body)
assert %{
id: "11349865-1b7b-454a-b999-6c4059888a78",
name: "Publisher",
inserted_at: "2025-03-21T09:20:48.791539Z",
updated_at: "2025-03-21T09:20:48.791539Z",
} = publisher
refute Map.has_key?(publisher, :dbi_server_id)
end
end
end
@@ -26,17 +26,13 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorSyncTest do
updated_at: "2025-03-21T09:20:48.791539Z"
}
{:ok, inserted_at, 0} = DateTime.from_iso8601(author[:inserted_at])
{:ok, updated_at, 0} = DateTime.from_iso8601(author[:updated_at])
assert :ok = AuthorSync.create_update(author, server.id)
assert {:ok, saved_author} = Metadata.get_author_by_id(author.id)
author =
author
|> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_at)
|> Map.delete(:author_alias_registry_id)
|> datetime_from_iso8601_for_map()
assert get_submap(saved_author, author) == author
assert nil != saved_author.author_alias_registry_id
@@ -56,16 +52,12 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorSyncTest do
updated_at: "2025-03-21T09:20:48.791539Z"
}
{:ok, inserted_at, 0} = DateTime.from_iso8601(author[:inserted_at])
{:ok, updated_at, 0} = DateTime.from_iso8601(author[:updated_at])
assert :ok = AuthorSync.create_update(author, server.id)
assert {:ok, saved_author} = Metadata.get_author_by_id(author.id)
author =
author
|> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_at)
|> datetime_from_iso8601_for_map()
assert get_submap(saved_author, author) == author
assert nil != saved_author.author_alias_registry_id
@@ -88,7 +80,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorSyncTest do
assert :ok = AuthorSync.create_update(author_attrs, server.id)
assert {:ok, saved_author} = Metadata.get_author_by_id(author.id)
assert author = saved_author
assert get_submap(author, author_attrs) == get_submap(saved_author, author_attrs)
end
test "the local existing author wouldn't updated", %{user: user} do
@@ -32,20 +32,17 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookSyncTest do
updated_at: "2025-04-01T18:14:25.754055Z"
}
{:ok, inserted_at, 0} = DateTime.from_iso8601(book[:inserted_at])
{:ok, updated_at, 0} = DateTime.from_iso8601(book[:updated_at])
assert :ok = BookSync.create_update(book, server.id)
assert {:ok, saved_book} = Metadata.get_book_by_id(book.id)
book =
book
|> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_at)
|> datetime_from_iso8601_for_map()
|> Map.replace(:published, Date.from_iso8601!(book[:published]))
assert nil != saved_book.book_editions_registry_id
assert server.id == saved_book.dbi_server_id
assert get_submap(saved_book, book) == book
end
test "a new book has the same editions registry" do
@@ -68,20 +65,17 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookSyncTest do
book_editions_registry_id: "1ed81d9e-3757-472a-aa3b-44fcdfbe770a"
}
{:ok, inserted_at, 0} = DateTime.from_iso8601(book[:inserted_at])
{:ok, updated_at, 0} = DateTime.from_iso8601(book[:updated_at])
assert :ok = BookSync.create_update(book, server.id)
assert {:ok, saved_book} = Metadata.get_book_by_id(book.id)
book =
book
|> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_at)
|> datetime_from_iso8601_for_map()
|> Map.replace(:published, Date.from_iso8601!(book[:published]))
assert nil != saved_book.book_editions_registry_id
assert server.id == saved_book.dbi_server_id
assert get_submap(saved_book, book) == book
end
test "a new book with relationships will be created" do
@@ -166,10 +160,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookSyncTest do
}
assert :ok = BookSync.create_update(book_attrs, server.id)
assert {:ok, saved_book} = Metadata.get_book_by_id(book.id)
assert {:ok, _saved_book} = Metadata.get_book_by_id(book.id)
end
test "the local existing book wouldn't updated", %{user: user} do
test "the local existing book wouldn't updated" do
server = generate(dbi_server(url: @test_server_endpoint))
book = generate(book())
@@ -195,7 +189,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookSyncTest do
assert saved_book.dbi_server_id == nil
end
test "old book wouldn't be sync", %{user: user} do
test "old book wouldn't be sync" do
server = generate(dbi_server(url: @test_server_endpoint))
book = generate(book())
@@ -347,11 +341,4 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookSyncTest do
assert Enum.all?(old_roles, fn id -> id not in new_roles end)
end
end
defp get_ids(records) do
Enum.map(records, fn record -> get_id(record) end)
end
defp get_id(record) when is_struct(record), do: record.id
defp get_id(record) when is_map(record), do: record[:id]
end
@@ -17,11 +17,11 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerSyncTest do
server = generate(dbi_server(url: @test_server_endpoint))
dbi_server = %{
id: "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
name: "Test",
url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z"
id: "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
name: "Test",
url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z"
}
{:ok, inserted_at, 0} = DateTime.from_iso8601(dbi_server[:inserted_at])
@@ -35,27 +35,30 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerSyncTest do
|> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_at)
assert dbi_server = saved_dbi_server
assert dbi_server == get_submap(saved_dbi_server, dbi_server)
assert server.id == saved_dbi_server.dbi_server_id
end
test "update an existing dbi_server", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint))
{:ok, dbi_server} = Metadata.create_dbi_server("Test", "http://localhost:4001", false, actor: user)
{:ok, dbi_server} =
Metadata.create_dbi_server("Test", "http://localhost:4001", false, actor: user)
dbi_server_attrs = %{
id: dbi_server.id,
name: "Test",
url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z"
id: dbi_server.id,
name: "Test",
url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z"
}
assert :ok = DBIServerSync.create_update(dbi_server_attrs, server.id)
assert {:ok, saved_dbi_server} = Metadata.get_dbi_server_by_id(dbi_server.id)
assert dbi_server_attrs = saved_dbi_server
assert datetime_from_iso8601_for_map(dbi_server_attrs) ==
get_submap(saved_dbi_server, dbi_server_attrs)
assert server.id == saved_dbi_server.dbi_server_id
end
end
@@ -23,9 +23,6 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.PublisherSyncTest do
updated_at: "2025-03-21T09:20:48.791539Z"
}
{:ok, inserted_at, 0} = DateTime.from_iso8601(publisher[:inserted_at])
{:ok, updated_at, 0} = DateTime.from_iso8601(publisher[:updated_at])
assert :ok = PublisherSync.create_update(publisher, server.id)
assert {:ok, saved_publisher} = Metadata.get_publisher_by_id(publisher.id)
@@ -16,10 +16,10 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncAuthorsTaskTest do
test "sync authors", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint))
{:ok, author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
{:ok, author} = Metadata.create_author("Author2", "An description", nil, nil, actor: user)
{:ok, _author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
{:ok, _author} = Metadata.create_author("Author2", "An description", nil, nil, actor: user)
assert server = SyncAuthorsTask.sync(server)
assert ^server = SyncAuthorsTask.sync(server)
end
end
end
@@ -2,7 +2,6 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncBookTaskTest do
use DecentralisedBookIndex.DataCase
alias DecentralisedBookIndex.SyncTasks.SyncBooksTask
alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.TestEndpoints
@test_server_endpoint TestEndpoints.test_api_endpoint()
@@ -14,7 +13,7 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncBookTaskTest do
_book = generate(book())
_book = generate(book())
assert server = SyncBooksTask.sync(server)
assert ^server = SyncBooksTask.sync(server)
end
end
end
@@ -19,7 +19,7 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncDBIServerTaskTest do
{:ok, _dbi_server} = Metadata.create_dbi_server("Test", "http://localhost:4001", false, actor: user)
{:ok, _dbi_server} = Metadata.create_dbi_server("Test2", "http://localhost:4001", false, actor: user)
assert server = SyncDBIServerTask.sync(server)
assert ^server = SyncDBIServerTask.sync(server)
end
end
end
@@ -19,7 +19,7 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncPublishersTaskTest do
{:ok, _publisher} = Metadata.create_publisher("Publisher", actor: user)
{:ok, _publisher} = Metadata.create_publisher("Publisher2", actor: user)
assert server = SyncPublishersTask.sync(server)
assert ^server = SyncPublishersTask.sync(server)
end
end
end
@@ -3,9 +3,6 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncServerTaskTest do
alias DecentralisedBookIndex.SyncTasks.SyncServerTask
alias DecentralisedBookIndex.Sync.ApiClients.FetchJson
alias DecentralisedBookIndex.Sync.AuthorSync
alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.TestEndpoints
@@ -18,19 +15,19 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncServerTaskTest do
describe "sync all" do
test "servers", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint))
{:ok, author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
_server = generate(dbi_server(url: @test_server_endpoint))
{:ok, _author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
assert server = SyncServerTask.sync_all()
assert :ok = SyncServerTask.sync_all()
end
end
describe "sync one" do
test "server", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint))
{:ok, author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
{:ok, _author} = Metadata.create_author("Author", "An description", nil, nil, actor: user)
assert server = SyncServerTask.sync_one(server)
assert :ok = SyncServerTask.sync_one(server)
end
end
end
@@ -0,0 +1,45 @@
defmodule DecentralisedBookIndexWeb.Live.BookLive.IndexTest do
use DecentralisedBookIndexWeb.LiveCase, async: true
describe "Search by bid" do
setup do
%{
modes: [
"isbn",
"isbn13",
"asin"
],
books:
for _ <- 1..5 do
generate(book())
end
}
end
test "can search by an empty bid", %{conn: conn, modes: modes} do
for mode <- modes do
{:ok, _view, html} =
conn
|> live("/books?search_mode=#{mode}")
assert html =~ "Listing Books"
assert html =~ "No Books"
end
end
test "can search by a bid", %{conn: conn, modes: modes, books: books} do
[book | others] = books
for %{type: type, bid: bid} <- book.bids do
{:ok, view, html} =
conn
|> live("/books?query=#{bid}&search_mode=#{type}")
html = render(view)
assert html =~ book.title
refute html =~ "No Books"
end
end
end
end
@@ -0,0 +1,118 @@
defmodule DecentralisedBookIndexWeb.LiveViewsPermissionsForOtherServersDataTest do
use DecentralisedBookIndexWeb.LiveCase, async: true
alias DecentralisedBookIndex.TestEndpoints
@test_server_endpoint TestEndpoints.test_api_endpoint()
setup do
moderator = generate(user(role: :moderator))
admin = generate(user(role: :admin))
server = generate(dbi_server(url: @test_server_endpoint))
%{
server: server,
moderator: moderator,
admin: admin,
users: [moderator, admin]
}
end
describe "Book Edit /books/:id/edit" do
setup %{server: server} do
%{book: generate(book(dbi_server_id: server.id))}
end
test "can't edit other servers' data", %{conn: conn, users: users, book: book} do
for user <- users do
redirect_url = "/books/#{book.id}"
assert {:error,
{:redirect,
%{flash: %{"error" => "Can't edit other server's data!"}, to: ^redirect_url}}} =
conn
|> log_in_user(user)
|> live("/books/#{book.id}/edit")
end
end
end
describe "Author Edit /authors/:id/edit" do
setup %{server: server} do
%{author: generate(author(dbi_server_id: server.id))}
end
test "can't edit other servers' data", %{conn: conn, users: users, author: author} do
for user <- users do
redirect_url = "/authors/#{author.id}"
assert {:error,
{:redirect,
%{flash: %{"error" => "Can't edit other server's data!"}, to: ^redirect_url}}} =
conn
|> log_in_user(user)
|> live("/authors/#{author.id}/edit")
end
end
end
describe "Publisher Edit /publishers/:id/edit" do
setup %{server: server} do
%{publisher: generate(publisher(dbi_server_id: server.id))}
end
test "can't edit other servers' data", %{conn: conn, admin: admin, publisher: publisher} do
for user <- [admin] do
redirect_url = "/publishers/#{publisher.id}"
assert {:error,
{:redirect,
%{flash: %{"error" => "Can't edit other server's data!"}, to: ^redirect_url}}} =
conn
|> log_in_user(user)
|> live("/publishers/#{publisher.id}/edit")
end
end
end
describe "Server Edit /servers/:id/edit" do
setup %{server: server} do
%{dbi_server: generate(dbi_server(dbi_server_id: server.id))}
end
test "can edit other servers' data", %{conn: conn, admin: admin, dbi_server: dbi_server} do
for user <- [admin] do
assert {:ok, _view, _html} =
conn
|> log_in_user(user)
|> live("/servers/#{dbi_server.id}/edit")
end
end
test "can edit this server data", %{conn: conn, admin: admin} do
dbi_server = generate(dbi_server())
for user <- [admin] do
assert {:ok, view, _html} =
conn
|> log_in_user(user)
|> live("/servers/#{dbi_server.id}/edit")
assert has_element?(view, "input#dbi_server_name")
assert has_element?(view, "input#dbi_server_url")
end
end
test "can edit other servers' data, but only sync_on?", %{conn: conn, admin: admin, dbi_server: dbi_server} do
for user <- [admin] do
assert {:ok, view, _html} =
conn
|> log_in_user(user)
|> live("/servers/#{dbi_server.id}/edit")
refute has_element?(view, "input#dbi_server_name")
refute has_element?(view, "input#dbi_server_url")
end
end
end
end
@@ -0,0 +1,514 @@
defmodule DecentralisedBookIndexWeb.LiveViewsPermissionsTest do
use DecentralisedBookIndexWeb.LiveCase, async: true
setup do
user = generate(user(role: :user))
moderator = generate(user(role: :moderator))
admin = generate(user(role: :admin))
%{
user: user,
moderator: moderator,
admin: admin,
users: [nil, user, moderator, admin]
}
end
describe "Book Index /books" do
test "can be accessed by any user", %{conn: conn, users: users} do
for user <- users do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/books")
assert html =~ "Listing Books"
end
end
end
describe "Book Show /books/:id" do
setup do
%{book: generate(book())}
end
test "can be accessed by any user", %{conn: conn, users: users, book: book} do
for user <- users do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/books/#{book.id}")
assert html =~ book.title
end
end
end
describe "Book Edit /books/:id/new" do
test "can't be accessed by regular user", %{conn: conn, user: user} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/books/new")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/books/new")
assert html =~ "New Book"
end
end
end
describe "Book Edit /books/:id/edit" do
setup do
%{book: generate(book())}
end
test "can't be accessed by regular user", %{conn: conn, user: user, book: book} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/books/#{book.id}/edit")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin,
book: book
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/books/#{book.id}/edit")
assert html =~ "Edit Book"
end
end
end
describe "Author Index /authors" do
test "can be accessed by any user", %{conn: conn, users: users} do
for user <- users do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/authors")
assert html =~ "Listing Authors"
end
end
end
describe "Author Show /authors/:id" do
setup do
%{author: generate(author())}
end
test "can be accessed by any user", %{conn: conn, users: users, author: author} do
for user <- users do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/authors/#{author.id}")
assert html =~ author.name
end
end
end
describe "Author Edit /authors/:id/new" do
test "can't be accessed by regular user", %{conn: conn, user: user} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/authors/new")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/authors/new")
assert html =~ "New Author"
end
end
end
describe "Author Edit /authors/:id/edit" do
setup do
%{author: generate(author())}
end
test "can't be accessed by regular user", %{conn: conn, user: user, author: author} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/authors/#{author.id}/edit")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin,
author: author
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/authors/#{author.id}/edit")
assert html =~ "Edit Author"
end
end
end
describe "Publisher Index /publishers" do
test "can't be accessed by regular user", %{conn: conn, user: user} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/publishers")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin
} do
for user <- [moderator, admin] do
assert {:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/publishers")
assert html =~ "Listing Publishers"
end
end
end
describe "Publisher Show /publishers/:id" do
setup do
%{publisher: generate(publisher())}
end
test "can't be accessed by regular user", %{conn: conn, user: user, publisher: publisher} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/publishers/#{publisher.id}")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin,
publisher: publisher
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/publishers/#{publisher.id}")
assert html =~ publisher.name
end
end
end
describe "Publisher Edit /publishers/:id/new" do
test "can't be accessed by regular user", %{conn: conn, user: user} do
for user <- [nil, user] do
{:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/publishers/new")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/publishers/new")
assert html =~ "New Publisher"
end
end
end
describe "Publisher Edit /publishers/:id/edit" do
setup do
%{publisher: generate(publisher())}
end
test "can't be accessed by regular user", %{conn: conn, user: user, publisher: publisher} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/publishers/#{publisher.id}/edit")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin,
publisher: publisher
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/publishers/#{publisher.id}/edit")
assert html =~ "Edit Publisher"
end
end
end
describe "Server Index /servers" do
test "can't be accessed by regular user", %{
conn: conn,
user: user
} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/servers")
end
end
test "can be accessed by moderator and admin", %{
conn: conn,
moderator: moderator,
admin: admin
} do
for user <- [moderator, admin] do
assert {:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/servers")
assert html =~ "Listing Servers"
end
end
end
describe "Server Show /servers/:id" do
setup do
%{server: generate(dbi_server())}
end
test "can't be accessed by non-moderator user", %{
conn: conn,
user: user,
server: server
} do
for user <- [nil, user] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/servers/#{server.id}")
end
end
test "can be accessed by admin", %{
conn: conn,
moderator: moderator,
admin: admin,
server: server
} do
for user <- [moderator, admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/servers/#{server.id}")
assert html =~ server.name
end
end
end
describe "Server Edit /servers/:id/new" do
test "can't be accessed by non-admin user", %{conn: conn, user: user, moderator: moderator} do
for user <- [nil, user, moderator] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/servers/new")
end
end
test "can be accessed by admin", %{conn: conn, admin: admin} do
for user <- [admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/servers/new")
assert html =~ "New Server"
end
end
end
describe "Server Edit /servers/:id/edit" do
setup do
%{server: generate(dbi_server())}
end
test "can't be accessed by non-admin user", %{
conn: conn,
user: user,
moderator: moderator,
server: server
} do
for user <- [nil, user, moderator] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/servers/#{server.id}/edit")
end
end
test "can be accessed by admin", %{conn: conn, admin: admin, server: server} do
for user <- [admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/servers/#{server.id}/edit")
assert html =~ "Edit Server"
end
end
end
describe "ObanWeb /oban" do
test "can't be accessed by regular user and moderator", %{
conn: conn,
user: user,
moderator: moderator
} do
for user <- [nil, user, moderator] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/oban")
end
end
end
describe "User Index /users" do
test "can't be accessed by regular user and moderator", %{
conn: conn,
user: user,
moderator: moderator
} do
for user <- [nil, user, moderator] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/users")
end
end
test "can be accessed by admin", %{conn: conn, admin: admin} do
for user <- [admin] do
assert {:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/users")
assert html =~ "Listing Users"
end
end
end
describe "User Show /users/:id" do
test "can't be accessed by non-admin user", %{
conn: conn,
user: regular_user,
moderator: moderator
} do
for user <- [nil, regular_user, moderator] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/users/#{regular_user.id}")
end
end
test "can be accessed by admin", %{conn: conn, admin: admin} do
for user <- [admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/users/#{user.id}")
assert html =~ user.email.string
end
end
end
describe "User Edit /users/:id/edit" do
test "can't be accessed by non-admin user", %{
conn: conn,
user: regular_user,
moderator: moderator
} do
for user <- [nil, regular_user, moderator] do
assert {:error, {:redirect, %{flash: %{"error" => "Unauthorized!"}, to: "/"}}} =
conn
|> log_in_user(user)
|> live("/users/#{regular_user.id}/edit")
end
end
test "can be accessed by admin", %{conn: conn, admin: admin} do
for user <- [admin] do
{:ok, _view, html} =
conn
|> log_in_user(user)
|> live("/users/#{user.id}/edit")
assert html =~ "Edit User"
end
end
end
end
+14 -7
View File
@@ -35,13 +35,16 @@ defmodule DecentralisedBookIndex.Generators do
generate(user(role: :moderator))
end)
dbi_server_id = opts[:dbi_server_id] || nil
changeset_generator(
Metadata.Author,
:create,
defaults: [
name: sequence(:name, &"Author #{&1}"),
description: sequence(:name, &"Description #{&1}"),
author_alias_registry_id: nil
author_alias_registry_id: nil,
dbi_server_id: dbi_server_id
],
overrides: opts,
actor: actor
@@ -83,16 +86,17 @@ defmodule DecentralisedBookIndex.Generators do
url =
opts[:url] ||
once(:default_actor, fn ->
"http://localhost:4002"
end)
"http://localhost:4002"
dbi_server_id = opts[:dbi_server_id] || nil
changeset_generator(
Metadata.DBIServer,
:create,
defaults: [
name: sequence(:name, &"Server #{&1}"),
url: url
url: url,
dbi_server_id: dbi_server_id
],
overrides: opts,
actor: actor
@@ -115,7 +119,7 @@ defmodule DecentralisedBookIndex.Generators do
count =
opts[:count] || 2
types = ["isbn10", "isbn13", "asin"]
types = ["isbn", "isbn13", "asin"]
if count > length(types), do: count = length(types)
@@ -136,11 +140,14 @@ defmodule DecentralisedBookIndex.Generators do
generate(user(role: :moderator))
end)
dbi_server_id = opts[:dbi_server_id] || nil
changeset_generator(
Metadata.Publisher,
:create,
defaults: [
name: sequence(:name, &"Publisher #{&1}")
name: sequence(:name, &"Publisher #{&1}"),
dbi_server_id: dbi_server_id
],
overrides: opts,
actor: actor
+18
View File
@@ -40,4 +40,22 @@ defmodule DecentralisedBookIndex.Helpers do
|> DateTime.add(seconds)
|> DateTime.to_iso8601()
end
def get_ids(records) do
Enum.map(records, fn record -> get_id(record) end)
end
def get_id(record) when is_struct(record), do: record.id
def get_id(record) when is_map(record), do: record[:id]
@doc """
Logs the given `user` into the `conn`.
It returns an updated `conn`.
"""
def log_in_user(conn, user) do
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> AshAuthentication.Plug.Helpers.store_in_session(user)
end
end
+75
View File
@@ -0,0 +1,75 @@
defmodule DecentralisedBookIndexWeb.LiveCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use DecentralisedBookIndex.DataCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
alias DecentralisedBookIndex.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import DecentralisedBookIndex.DataCase
import DecentralisedBookIndex.Generators
import DecentralisedBookIndex.Helpers
# The default endpoint for testing
@endpoint DecentralisedBookIndexWeb.Endpoint
use DecentralisedBookIndexWeb, :verified_routes
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import DecentralisedBookIndexWeb.ConnCase
import Phoenix.LiveViewTest
end
end
setup tags do
DecentralisedBookIndex.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
@doc """
Sets up the sandbox based on the test tags.
"""
def setup_sandbox(tags) do
pid =
Ecto.Adapters.SQL.Sandbox.start_owner!(DecentralisedBookIndex.Repo,
shared: not tags[:async]
)
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
@doc """
A helper that transforms changeset errors into a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end