56 Commits
Author SHA1 Message Date
KKlochko 18dbcadce6 Update the version.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-06-12 19:41:27 +03:00
KKlochko f1a4be7d23 Add Req to the dependencies. 2025-06-12 19:38:32 +03:00
KKlochko 2ec60455cf Update the version.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-06-06 20:29:47 +03:00
KKlochko c9409996fe Fix the key mismatch to solve FOUC. 2025-06-06 20:28:00 +03:00
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
77 changed files with 2206 additions and 462 deletions
+14
View File
@@ -24,6 +24,20 @@ steps:
commands: commands:
- mix test - 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: volumes:
- name: mix - name: mix
temp: {} temp: {}
+3
View File
@@ -9,6 +9,9 @@
- change the values in `.env` and `.env_db` - change the values in `.env` and `.env_db`
you can use `mix phx.gen.secret` to generate secrets. you can use `mix phx.gen.secret` to generate secrets.
update the PHX_HOST to your domain name. 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 `docker compose up -d`
- run migrations `docker exec -it dbi bin/migrate` - run migrations `docker exec -it dbi bin/migrate`
- open the url (`PHX_HOST`) in a browser. - 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 bidSort from "./bidSort";
import authorRoleSort from "./authorRoleSort"; import authorRoleSort from "./authorRoleSort";
import darkModeHook from "../vendor/dark_mode"
let DarkThemeToggle = darkModeHook;
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, { let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}, params: {_csrf_token: csrfToken},
hooks: { bidSort, authorRoleSort } hooks: { bidSort, authorRoleSort, DarkThemeToggle }
}) })
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
+1
View File
@@ -6,6 +6,7 @@ const fs = require("fs")
const path = require("path") const path = require("path")
module.exports = { module.exports = {
darkMode: 'class',
content: [ content: [
"../deps/ash_authentication_phoenix/**/*.*ex", "../deps/ash_authentication_phoenix/**/*.*ex",
"./js/**/*.js", "./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, repo: DecentralisedBookIndex.Repo,
queues: [default: 14], queues: [default: 14],
plugins: [ plugins: [
Oban.Plugins.Pruner, # A job will be pruned after 7 days.
{Oban.Plugins.Pruner, max_age: 7*24*60*60},
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"@daily", DecentralisedBookIndex.SyncWorker}, {"@daily", DecentralisedBookIndex.SyncWorker},
+4 -2
View File
@@ -17,8 +17,10 @@ services:
web: web:
# to build the image # to build the image
build: #build:
context: . # context: .
# to pull the pre-built image
image: git.kklochko.space/kklochko/decentralised_book_index:latest
container_name: dbi container_name: dbi
ports: ports:
- 4000:4000 - 4000:4000
+5 -4
View File
@@ -6,8 +6,9 @@ defmodule DecentralisedBookIndex.Metadata do
json_api do json_api do
routes do routes do
base_route "/books", Metadata.Book do base_route "/books", Metadata.Book do
get :read get :by_id
index :search index :search
index :search_by_bid, route: "/search-by-bid"
related :bids, :read, primary?: true related :bids, :read, primary?: true
related :author_roles, :read, primary?: true related :author_roles, :read, primary?: true
@@ -15,17 +16,17 @@ defmodule DecentralisedBookIndex.Metadata do
end end
base_route "/authors", Metadata.Author do base_route "/authors", Metadata.Author do
get :read get :by_id
index :search index :search
end end
base_route "/publishers", Metadata.Publisher do base_route "/publishers", Metadata.Publisher do
get :read get :by_id
index :search index :search
end end
base_route "/servers", Metadata.DBIServer do base_route "/servers", Metadata.DBIServer do
get :read get :by_id
index :search index :search
end end
end end
@@ -25,12 +25,16 @@ defmodule DecentralisedBookIndex.Metadata.Author do
end end
end end
resource do
description "An author's metadata."
end
actions do actions do
defaults [:read, :destroy] defaults [:read, :destroy]
create :create do create :create do
primary? true 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 -> change fn changeset, context ->
actor = Map.get(context, :actor, nil) actor = Map.get(context, :actor, nil)
@@ -100,6 +104,7 @@ defmodule DecentralisedBookIndex.Metadata.Author do
end end
read :by_id do read :by_id do
description "Return the Author by its id."
argument :id, :uuid, allow_nil?: false argument :id, :uuid, allow_nil?: false
get? true get? true
filter expr(id == ^arg(:id)) filter expr(id == ^arg(:id))
@@ -136,7 +141,10 @@ defmodule DecentralisedBookIndex.Metadata.Author do
end end
read :search do read :search do
description "Return a list of Authors, optionally filtering by name."
argument :name, :ci_string do argument :name, :ci_string do
description "Return Authors, which names includes the name."
constraints allow_empty?: true constraints allow_empty?: true
default "" default ""
end end
@@ -21,8 +21,12 @@ defmodule DecentralisedBookIndex.Metadata.AuthorRole do
end end
end end
resource do
description "A metadata of an author's role."
end
actions do actions do
defaults [:read, :destroy] defaults [:destroy]
create :create do create :create do
primary? true primary? true
@@ -43,13 +47,20 @@ defmodule DecentralisedBookIndex.Metadata.AuthorRole do
end end
end end
read :read do
description "Return a list of AuthorRoles."
primary? true
end
read :by_id do read :by_id do
description "Return the AuthorRole by its id."
argument :id, :uuid, allow_nil?: false argument :id, :uuid, allow_nil?: false
get? true get? true
filter expr(id == ^arg(:id)) filter expr(id == ^arg(:id))
end end
read :by_book_id do read :by_book_id do
description "Return the AuthorRole by its BookId."
argument :book_id, :uuid, allow_nil?: false argument :book_id, :uuid, allow_nil?: false
filter expr(book_id == ^arg(:book_id)) filter expr(book_id == ^arg(:book_id))
end end
+19 -4
View File
@@ -25,6 +25,10 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end end
end end
resource do
description "A book's metadata."
end
actions do actions do
defaults [:read, :destroy] defaults [:read, :destroy]
@@ -142,6 +146,7 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end end
read :by_id do read :by_id do
description "Return the Book by its id."
argument :id, :uuid, allow_nil?: false argument :id, :uuid, allow_nil?: false
get? true get? true
filter expr(id == ^arg(:id)) filter expr(id == ^arg(:id))
@@ -174,15 +179,24 @@ defmodule DecentralisedBookIndex.Metadata.Book do
prepare fn query, context -> prepare fn query, context ->
author = query.arguments.author 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))) |> Ash.Query.filter(expr(exists(author_roles, author_id in ^author_ids)))
end end
pagination offset?: true, default_limit: 10
end end
read :search do read :search do
description "Return a list of Books, optionally filtering by title."
argument :query, :ci_string do argument :query, :ci_string do
description "Return Books, which titles includes the query."
constraints allow_empty?: true constraints allow_empty?: true
default "" default ""
end end
@@ -193,8 +207,10 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end end
read :search_by_bid do read :search_by_bid do
description "Return a list of Books, optionally filtering by BookId."
argument :type, :string, allow_nil?: false 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)))) filter expr(exists(bids, type == ^arg(:type) and contains(bid, ^arg(:bid))))
@@ -363,7 +379,6 @@ defmodule DecentralisedBookIndex.Metadata.Book do
end end
belongs_to :publisher, Metadata.Publisher do belongs_to :publisher, Metadata.Publisher do
allow_nil? false
public? true public? true
end end
@@ -26,8 +26,12 @@ defmodule DecentralisedBookIndex.Metadata.BookId do
end end
end end
resource do
description "A book's identifier."
end
actions do actions do
defaults [:read, :destroy] defaults [:destroy]
create :create do create :create do
primary? true primary? true
@@ -39,7 +43,13 @@ defmodule DecentralisedBookIndex.Metadata.BookId do
accept [:id, :type, :bid, :order] accept [:id, :type, :bid, :order]
end end
read :read do
description "Return a list of BookIds."
primary? true
end
read :by_id do read :by_id do
description "Return the BookId by its id."
argument :id, :uuid, allow_nil?: false argument :id, :uuid, allow_nil?: false
get? true get? true
filter expr(id == ^arg(:id)) filter expr(id == ^arg(:id))
@@ -21,12 +21,16 @@ defmodule DecentralisedBookIndex.Metadata.DBIServer do
end end
end end
resource do
description "An DBIServer's metadata."
end
actions do actions do
defaults [:read, :destroy] defaults [:read, :destroy]
create :create do create :create do
primary? true primary? true
accept [:name, :url, :sync_on?] accept [:name, :url, :sync_on?, :dbi_server_id]
end end
create :sync_create do create :sync_create do
@@ -43,13 +47,17 @@ defmodule DecentralisedBookIndex.Metadata.DBIServer do
end end
read :by_id do read :by_id do
description "Return the DBIServer by its id."
argument :id, :uuid, allow_nil?: false argument :id, :uuid, allow_nil?: false
get? true get? true
filter expr(id == ^arg(:id)) filter expr(id == ^arg(:id))
end end
read :search do read :search do
description "Return a list of DBIServers, optionally filtering by name."
argument :name, :ci_string do argument :name, :ci_string do
description "Return DBIServers, which names includes the name."
constraints allow_empty?: true constraints allow_empty?: true
default "" default ""
end end
@@ -95,7 +103,9 @@ defmodule DecentralisedBookIndex.Metadata.DBIServer do
end end
relationships do 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, Metadata.Author
has_many :author_alias_registries, Metadata.AuthorAliasRegistry has_many :author_alias_registries, Metadata.AuthorAliasRegistry
@@ -22,26 +22,39 @@ defmodule DecentralisedBookIndex.Metadata.Publisher do
end end
end end
resource do
description "An publisher's metadata."
end
actions do actions do
defaults [:read, :destroy] defaults [:destroy]
create :create do create :create do
primary? true primary? true
accept [:name] accept [:name, :dbi_server_id]
end end
create :sync_create do create :sync_create do
accept [:id, :name, :inserted_at, :updated_at, :dbi_server_id] accept [:id, :name, :inserted_at, :updated_at, :dbi_server_id]
end end
read :read do
description "Return a list of Publishers."
primary? true
end
read :by_id do read :by_id do
description "Return the Publisher by its id."
argument :id, :uuid, allow_nil?: false argument :id, :uuid, allow_nil?: false
get? true get? true
filter expr(id == ^arg(:id)) filter expr(id == ^arg(:id))
end end
read :search do read :search do
description "Return a list of Publishers, optionally filtering by name."
argument :name, :ci_string do argument :name, :ci_string do
description "Return Publishers, which names includes the name."
constraints allow_empty?: true constraints allow_empty?: true
default "" default ""
end end
@@ -20,6 +20,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorTransformer do
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"]) dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
} }
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs} {:ok, attrs}
end end
end end
end
@@ -26,6 +26,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookTransformer do
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"]) dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
} }
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs} {:ok, attrs}
end end
end end
end
@@ -13,9 +13,15 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformer do
name: get_in(json_body, ["attributes", "name"]), name: get_in(json_body, ["attributes", "name"]),
url: get_in(json_body, ["attributes", "url"]), url: get_in(json_body, ["attributes", "url"]),
inserted_at: get_in(json_body, ["attributes", "inserted_at"]), 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"])
} }
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs} {:ok, attrs}
end end
end end
end
@@ -17,6 +17,10 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.PublisherTransformer do
dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"]) dbi_server_id: get_in(json_body, ["attributes", "dbi_server_id"])
} }
if is_nil(attrs[:dbi_server_id]) do
{:ok, attrs |> Map.delete(:dbi_server_id)}
else
{:ok, attrs} {:ok, attrs}
end end
end end
end
@@ -50,7 +50,11 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
data-cancel={JS.exec(@on_cancel, "phx-remove")} data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden" 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 <div
class="fixed inset-0 overflow-y-auto" class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"} aria-labelledby={"#{@id}-title"}
@@ -425,7 +429,8 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
class={[ 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", "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-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} {@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea> >{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
@@ -449,7 +454,8 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
class={[ 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", "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-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} {@rest}
/> />
@@ -546,8 +552,8 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
~H""" ~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0 "> <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"> <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-gray-50 dark:bg-gray-700 dark:text-gray-400"> <thead class="text-xs text-gray-700 uppercase bg-slate-200 dark:bg-gray-700 dark:text-gray-400">
<tr> <tr>
<th :for={col <- @col} class="p-4 font-normal">{col[:label]}</th> <th :for={col <- @col} class="p-4 font-normal">{col[:label]}</th>
<th :if={@action != []} class="relative p-0 pb-4"> <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"} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-zinc-100 text-sm leading-6 text-zinc-700" 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 <td
:for={{col, i} <- Enum.with_index(@col)} :for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)} phx-click={@row_click && @row_click.(row)}
@@ -568,7 +578,10 @@ defmodule DecentralisedBookIndexWeb.CoreComponents do
> >
<div class="block py-4 pr-6"> <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="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))} {render_slot(col, @row_item.(row))}
</span> </span>
</div> </div>
@@ -10,6 +10,14 @@
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <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 defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script> </script>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
</head> </head>
<body class="bg-white dark:bg-gray-900"> <body class="bg-white dark:bg-gray-900">
{@inner_content} {@inner_content}
@@ -34,6 +34,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents do
alias MyComponents.SelectAuthorAlias alias MyComponents.SelectAuthorAlias
import MyComponents.SelectedAuthorAlias, only: [selected_author_alias: 1] import MyComponents.SelectedAuthorAlias, only: [selected_author_alias: 1]
import MyComponents.DarkModeSwitcher, only: [dark_mode_switcher: 1]
end end
end end
end end
@@ -9,8 +9,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.AuthorCard do
def author_card(assigns) do def author_card(assigns) do
~H""" ~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"> <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) do %> <%= 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"> <div class="flex justify-end px-4 pt-4">
<button <button
id={"dropdownButton#{@author.id}"} id={"dropdownButton#{@author.id}"}
@@ -32,7 +32,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.AuthorCard do
<!-- Dropdown menu --> <!-- Dropdown menu -->
<div <div
id={"dropdown#{@author.id}"} 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"> <ul class="py-2" aria-labelledby="dropdownButton">
<li> <li>
@@ -63,7 +63,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.AuthorCard do
</div> </div>
</div> </div>
<% end %> <% 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}"}> <.link navigate={~p"/authors/#{@author.id}"}>
<%= if @author.avatar_url != nil do %> <%= if @author.avatar_url != nil do %>
<img <img
@@ -9,8 +9,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookCard do
def book_card(assigns) do def book_card(assigns) do
~H""" ~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"> <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) do %> <%= 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"> <div class="flex justify-end px-4 pt-4">
<button <button
id={"dropdownButton#{@book.id}"} id={"dropdownButton#{@book.id}"}
@@ -32,7 +32,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookCard do
<!-- Dropdown menu --> <!-- Dropdown menu -->
<div <div
id={"dropdown#{@book.id}"} 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"> <ul class="py-2" aria-labelledby="dropdownButton">
<li> <li>
@@ -63,7 +63,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookCard do
</div> </div>
</div> </div>
<% end %> <% 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}"}> <.link navigate={~p"/books/#{@book.id}"}>
<%= if @book.cover_image_url != nil do %> <%= if @book.cover_image_url != nil do %>
<img class="w-36 mb-3 shadow-lg" src={@book.cover_image_url} alt={"#{@book.title} image"} /> <img class="w-36 mb-3 shadow-lg" src={@book.cover_image_url} alt={"#{@book.title} image"} />
@@ -14,26 +14,58 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
def book_search(assigns) do def book_search(assigns) do
~H""" ~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="flex items-center justify-center p-4 flex-row space-y-0 space-x-4">
<div class="w-full grow"> <div class="w-full grow">
<form class="flex items-center" phx-change="search" phx-submit="search"> <form class="flex items-center" phx-change="search" phx-submit="search">
<label for="simple-search" class="sr-only">Search</label> <label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full"> <div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <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"> <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" /> 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> </svg>
</div> </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> </div>
</form> </form>
</div> </div>
<%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %> <%= if @current_user != nil and Role.can_moderate?(@current_user.role) do %>
<.link patch={~p"/books/new"} class="flex-shrink-0 my-auto"> <.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"> <button
<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"> type="button"
<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" /> 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> </svg>
Add book Add book
</button> </button>
@@ -58,17 +90,42 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
def dropdown_settings(assigns) do def dropdown_settings(assigns) do
~H""" ~H"""
<div class="flex justify-end px-4"> <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"> <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> <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"> <svg
<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"/> 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> </svg>
</button> </button>
<!-- Dropdown menu --> <!-- 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
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"> <div class="flex items-center space-x-3 md:w-auto">
<form phx-change="change-search-mode" class="w-full"> <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"> <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 %> <%= for {name, value} <- @search_mode_options do %>
<%= if value == @search_mode do %> <%= if value == @search_mode do %>
<option value={value} selected>{name}</option> <option value={value} selected>{name}</option>
@@ -81,7 +138,11 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.BookSearch do
</div> </div>
<div class="flex items-center space-x-3 md:w-auto"> <div class="flex items-center space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full"> <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"> <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 %> <%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %> <%= if value == @selected_option do %>
<option value={value} selected>{name}</option> <option value={value} selected>{name}</option>
@@ -120,8 +120,20 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Buttons do
]} ]}
{@rest} {@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"> <svg
<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"/> 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" /> <path fill-rule="evenodd" d="M14 17h-4v-2h4v2Z" clip-rule="evenodd" />
</svg> </svg>
{render_slot(@inner_block)} {render_slot(@inner_block)}
@@ -147,9 +159,21 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Buttons do
]} ]}
{@rest} {@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"> <svg
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path> xmlns="http://www.w3.org/2000/svg"
<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> 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> </svg>
{render_slot(@inner_block)} {render_slot(@inner_block)}
</.primary_button> </.primary_button>
@@ -174,8 +198,22 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Buttons do
]} ]}
{@rest} {@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"> <svg
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/> 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> </svg>
{render_slot(@inner_block)} {render_slot(@inner_block)}
</.primary_button> </.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,25 +11,48 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.JustSearchResources
def just_search_resources(assigns) do def just_search_resources(assigns) do
~H""" ~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="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"> <div class="w-full grow">
<form class="flex items-center" phx-change="search" phx-submit="search"> <form class="flex items-center" phx-change="search" phx-submit="search">
<label for="simple-search" class="sr-only">Search</label> <label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full"> <div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <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"> <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" /> 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> </svg>
</div> </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> </div>
</form> </form>
</div> </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 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"> <div class="flex items-center w-full space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full"> <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"> <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 %> <%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %> <%= if value == @selected_option do %>
<option value={value} selected>{name}</option> <option value={value} selected>{name}</option>
@@ -11,6 +11,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Pagination do
assigns = assign(assigns, :current_page_number, get_current_page(assigns.page)) assigns = assign(assigns, :current_page_number, get_current_page(assigns.page))
~H""" ~H"""
<%= if AshPhoenix.LiveView.prev_page?(@page) or AshPhoenix.LiveView.next_page?(@page) do %>
<nav aria-label="Page navigation example" class="flex justify-center"> <nav aria-label="Page navigation example" class="flex justify-center">
<ul class="inline-flex -space-x-px text-base h-10 mx-auto"> <ul class="inline-flex -space-x-px text-base h-10 mx-auto">
<li class="pl-auto"> <li class="pl-auto">
@@ -19,7 +20,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Pagination do
kind="primary" kind="primary"
inverse inverse
patch={"#{@endpoint}?#{page_params_which(@page, @params, "prev")}"} 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" 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)} disabled={!AshPhoenix.LiveView.prev_page?(@page)}
> >
Previous Previous
@@ -28,14 +29,22 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Pagination do
<%= for number <- get_page_numbers(@page) do %> <%= for number <- get_page_numbers(@page) do %>
<%= if number == @current_page_number do %> <%= if number == @current_page_number do %>
<li> <li>
<a href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"} <a
href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"}
aria-current="page" 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> 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> </li>
<% else %> <% else %>
<li> <li>
<a href={"#{@endpoint}?#{page_params_number(@page_params, @params, number)}"} <a
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> 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> </li>
<% end %> <% end %>
<% end %> <% end %>
@@ -45,7 +54,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Pagination do
kind="primary" kind="primary"
inverse inverse
patch={"#{@endpoint}?#{page_params_which(@page, @params, "next")}"} 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" 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)} disabled={!AshPhoenix.LiveView.next_page?(@page)}
> >
Next Next
@@ -53,6 +62,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.Pagination do
</li> </li>
</ul> </ul>
</nav> </nav>
<% end %>
""" """
end end
@@ -17,27 +17,59 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SearchResources do
def search_resources(assigns) do def search_resources(assigns) do
~H""" ~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="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"> <div class="w-full grow">
<form class="flex items-center" phx-change="search" phx-submit="search"> <form class="flex items-center" phx-change="search" phx-submit="search">
<label for="simple-search" class="sr-only">Search</label> <label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full"> <div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <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"> <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" /> 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> </svg>
</div> </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> </div>
</form> </form>
</div> </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 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 %> <%= if can_add?(@current_user, @moderator_role) do %>
<.link patch={@resource_new_url}> <.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"> <button
<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"> type="button"
<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" /> 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> </svg>
Add {@resource_type} Add {@resource_type}
</button> </button>
@@ -45,7 +77,11 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SearchResources do
<% end %> <% end %>
<div class="flex items-center w-full space-x-3 md:w-auto"> <div class="flex items-center w-full space-x-3 md:w-auto">
<form phx-change="change-sort" class="w-full"> <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"> <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 %> <%= for {name, value} <- @select_options do %>
<%= if value == @selected_option do %> <%= if value == @selected_option do %>
<option value={value} selected>{name}</option> <option value={value} selected>{name}</option>
@@ -64,7 +100,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SearchResources do
defp can_add?(current_user, role) do defp can_add?(current_user, role) do
case 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) :moderator -> current_user != nil and Role.can_moderate?(current_user.role)
_ -> false _ -> false
end end
@@ -14,18 +14,54 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectAuthor do
<label for="simple-search" class="sr-only">Search</label> <label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full"> <div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <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"> <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> 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> </svg>
</div> </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> </div>
</form> </form>
<%= for author <- @page.results do %> <%= if Enum.empty?(@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"> <div class="flex justify-center ">
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">{author.name}</h5> <p class="text-lg font-semibold py-5 dark:text-white">
No Authors
</p>
</div> </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 %> <% end %>
</div> </div>
""" """
@@ -42,18 +42,26 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectAuthorAlias do
</div> </div>
</form> </form>
<%= 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 %> <%= for author_alias <- @page.results do %>
<div <div
phx-click="select-author-alias" phx-click="select-author-alias"
phx-target={@notify_component} phx-target={@notify_component}
phx-value-author-alias={author_alias.author_alias_registry_id} 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" 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"> <h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{author_alias.name} {author_alias.name}
</h5> </h5>
</div> </div>
<% end %> <% end %>
<% end %>
</div> </div>
""" """
end end
@@ -42,18 +42,26 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectBookEdition do
</div> </div>
</form> </form>
<%= 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 %> <%= for book_edition <- @page.results do %>
<div <div
phx-click="select-book-edition" phx-click="select-book-edition"
phx-target={@notify_component} phx-target={@notify_component}
phx-value-book-edition={book_edition.book_editions_registry_id} 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" 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"> <h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">
{book_edition.title} {book_edition.title}
</h5> </h5>
</div> </div>
<% end %> <% end %>
<% end %>
</div> </div>
""" """
end end
@@ -14,18 +14,53 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectPublisher do
<label for="simple-search" class="sr-only">Search</label> <label for="simple-search" class="sr-only">Search</label>
<div class="relative w-full"> <div class="relative w-full">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <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"> <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> 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> </svg>
</div> </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> </div>
</form> </form>
<%= for publisher <- @page.results do %> <%= if Enum.empty?(@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"> <div class="flex justify-center ">
<h5 class="mb-1 text-lg font-medium text-gray-900 dark:text-white px-2 pt-1">{publisher.name}</h5> <p class="text-lg font-semibold py-5 dark:text-white">
No Publishers
</p>
</div> </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 %> <% end %>
</div> </div>
""" """
@@ -20,8 +20,6 @@ defmodule DecentralisedBookIndexWeb.Components.MyComponents.SelectedAuthor do
errors = errors =
if Phoenix.Component.used_input?(assigns.author_form), do: assigns.author_form.errors, else: [] if Phoenix.Component.used_input?(assigns.author_form), do: assigns.author_form.errors, else: []
IO.inspect("errors")
IO.inspect(errors)
assigns = assigns =
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"> <span class="block text-sm text-gray-500 text-center dark:text-gray-400">
© 2025 Kostiantyn Klochko. All Rights Reserved. © 2025 Kostiantyn Klochko. All Rights Reserved.
</span> </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> </div>
</footer> </footer>
""" """
@@ -2,7 +2,8 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
use Phoenix.Component use Phoenix.Component
use DecentralisedBookIndexWeb, :verified_routes use DecentralisedBookIndexWeb, :verified_routes
import DecentralisedBookIndexWeb.Components.MyComponents.Buttons import DecentralisedBookIndexWeb.Components.MyComponents.{Buttons, DarkModeSwitcher}
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
alias DecentralisedBookIndex.Accounts.Role alias DecentralisedBookIndex.Accounts.Role
@@ -11,7 +12,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
def partial_navbar(assigns) do def partial_navbar(assigns) do
~H""" ~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"> <div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
<.link patch={~p"/books"} class="flex items-center"> <.link patch={~p"/books"} class="flex items-center">
<svg <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> <span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">DBI</span>
</.link> </.link>
<div class="flex items-center lg:order-2"> <div class="flex items-center lg:order-2">
<.dark_mode_switcher />
<.user_info current_user={@current_user} /> <.user_info current_user={@current_user} />
<button <button
data-collapse-toggle="mobile-menu-2" data-collapse-toggle="mobile-menu-2"
@@ -106,7 +108,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
</a> </a>
</li> </li>
<% end %> <% 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> <li>
<a <a
href={~p"/servers/"} href={~p"/servers/"}
@@ -153,7 +155,7 @@ defmodule DecentralisedBookIndexWeb.Components.MyPartials.Navbar do
<!-- Dropdown menu --> <!-- Dropdown menu -->
<div <div
id="dropdown-user-info" 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 %> <%= if @current_user.role != :user do %>
<p class="text-zinc-700 dark:text-white"> <p class="text-zinc-700 dark:text-white">
@@ -158,9 +158,6 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.FormComponent do
end end
def handle_event("select-author-alias", %{"author-alias" => author_alias_id}, socket) do 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 =
socket socket
|> update(:form, fn form -> |> update(:form, fn form ->
@@ -30,19 +30,11 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Index do
<% else %> <% else %>
<div class="flex flex-wrap flex-[3_1_auto]"> <div class="flex flex-wrap flex-[3_1_auto]">
<%= for author <- @page.results do %> <%= for author <- @page.results do %>
<.author_card <.author_card author={author} current_user={@current_user} />
author={author}
current_user={@current_user}
/>
<% end %> <% end %>
</div> </div>
<.pagination <.pagination endpoint={~p"/authors"} page={@page} page_params={@page_params} params={@params} />
endpoint={~p"/authors"}
page={@page}
page_params={@page_params}
params={@params}
/>
<% end %> <% end %>
""" """
end end
@@ -2,6 +2,7 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
use DecentralisedBookIndexWeb, :live_view use DecentralisedBookIndexWeb, :live_view
alias DecentralisedBookIndex.Accounts.Role alias DecentralisedBookIndex.Accounts.Role
alias DecentralisedBookIndex.Metadata
@impl true @impl true
def render(assigns) do def render(assigns) do
@@ -27,7 +28,7 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
</:actions> </:actions>
</.header> </.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 %> <%= if @author.avatar_url != nil do %>
<img <img
class="w-36 h-36 mb-3 rounded-full shadow-lg mx-auto md:float-left md:mr-4" 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"> <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> <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} {@author.description}
</p> </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>
</div> </div>
@@ -77,6 +97,19 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
</ul> </ul>
<% end %> <% 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> <.back navigate={~p"/authors"}>Back to authors</.back>
""" """
end end
@@ -87,16 +120,30 @@ defmodule DecentralisedBookIndexWeb.AuthorLive.Show do
end end
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(%{"id" => id} = params, _, socket) do
author = 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, {:noreply,
socket socket
|> assign(:page_title, page_title(socket.assigns.live_action)) |> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:author, author) |> assign(:author, author)
|> assign(:page_params, page_params)
|> assign(:page, page)
|> assign(:params, params)
|> assign(:alternative_names, alternative_names)} |> assign(:alternative_names, alternative_names)}
end end
@@ -21,19 +21,11 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
<%= if @form.source.type == :create do %> <%= if @form.source.type == :create do %>
<.input field={@form[:title]} type="text" label="Title" /> <.input field={@form[:title]} type="text" label="Title" />
<.input field={@form[:cover_image_url]} type="text" label="Cover image url" /> <.input field={@form[:cover_image_url]} type="text" label="Cover image url" />
<.input <.input field={@form[:description]} type="textarea" label="Description" />
field={@form[:description]}
type="textarea"
label="Description"
/>
<.bids_inputs form={@form} myself={@myself} /> <.bids_inputs form={@form} myself={@myself} />
<.author_roles_inputs form={@form} myself={@myself} notify_component={@myself} /> <.author_roles_inputs form={@form} myself={@myself} notify_component={@myself} />
<.input field={@form[:format]} type="text" label="Format" /> <.input field={@form[:format]} type="text" label="Format" />
<.input <.input field={@form[:language]} type="text" label="Language" />
field={@form[:language]}
type="text"
label="Language"
/>
<.input field={@form[:page_count]} type="number" label="Page count" /> <.input field={@form[:page_count]} type="number" label="Page count" />
<div> <div>
<.input field={@form[:publisher_id]} type="text" label="Publisher" type="hidden" show_errors?={false} /> <.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 %> <%= if @form.source.type == :update do %>
<.input field={@form[:title]} type="text" label="Title" /> <.input field={@form[:title]} type="text" label="Title" />
<.input field={@form[:cover_image_url]} type="text" label="Cover image url" /> <.input field={@form[:cover_image_url]} type="text" label="Cover image url" />
<.input <.input field={@form[:description]} type="textarea" label="Description" />
field={@form[:description]}
type="textarea"
label="Description"
/>
<.bids_inputs form={@form} myself={@myself} /> <.bids_inputs form={@form} myself={@myself} />
<.author_roles_inputs form={@form} myself={@myself} notify_component={@myself} /> <.author_roles_inputs form={@form} myself={@myself} notify_component={@myself} />
<.input field={@form[:format]} type="text" label="Format" /> <.input field={@form[:format]} type="text" label="Format" />
<.input <.input field={@form[:language]} type="text" label="Language" />
field={@form[:language]}
type="text"
label="Language"
/>
<.input field={@form[:page_count]} type="number" label="Page count" /> <.input field={@form[:page_count]} type="number" label="Page count" />
<div> <div>
<.input field={@form[:publisher_id]} type="text" label="Publisher" type="hidden" show_errors?={false} /> <.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}> <tbody phx-hook="bidSort" id="bidSort" phx-target={@myself}>
<.inputs_for :let={bid_form} field={@form[:bids]}> <.inputs_for :let={bid_form} field={@form[:bids]}>
<tr data-id={bid_form.index}> <tr data-id={bid_form.index}>
<td class="px-3 w-10 pt-2"> <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" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <svg
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4"/> 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> </svg>
</td> </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> <label for={bid_form[:type].id} class="hidden">Type</label>
<.input field={bid_form[:type]} /> <.input field={bid_form[:type]} />
</td> </td>
<td class="px-3"> <td class="px-3 align-top group">
<label for={bid_form[:bid].id} class="hidden">Id</label> <label for={bid_form[:bid].id} class="hidden">Id</label>
<.input field={bid_form[:bid]} /> <.input field={bid_form[:bid]} />
</td> </td>
@@ -167,8 +165,20 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
size="xs" size="xs"
inverse 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"> <svg
<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"/> 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> </svg>
</.button_link> </.button_link>
</td> </td>
@@ -229,12 +239,26 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
<tbody phx-hook="authorRoleSort" id="authorRoleSort" phx-target={@myself}> <tbody phx-hook="authorRoleSort" id="authorRoleSort" phx-target={@myself}>
<.inputs_for :let={author_roles_form} field={@form[:author_roles]}> <.inputs_for :let={author_roles_form} field={@form[:author_roles]}>
<tr data-id={author_roles_form.index}> <tr data-id={author_roles_form.index}>
<td class="px-3 w-10 pt-2"> <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" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <svg
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4"/> 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> </svg>
</td> </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> <label for={author_roles_form[:author_id].id} class="hidden">Type</label>
<.input field={author_roles_form[:author_id]} type="hidden" show_errors?={false} /> <.input field={author_roles_form[:author_id]} type="hidden" show_errors?={false} />
<.selected_author <.selected_author
@@ -244,7 +268,7 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
class="mt-2 w-full" class="mt-2 w-full"
/> />
</td> </td>
<td class="px-3"> <td class="px-3 align-top group">
<label for={author_roles_form[:role].id} class="hidden">Id</label> <label for={author_roles_form[:role].id} class="hidden">Id</label>
<.input field={author_roles_form[:role]} /> <.input field={author_roles_form[:role]} />
</td> </td>
@@ -257,8 +281,20 @@ defmodule DecentralisedBookIndexWeb.BookLive.FormComponent do
size="xs" size="xs"
inverse 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"> <svg
<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"/> 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> </svg>
</.button_link> </.button_link>
</td> </td>
@@ -30,19 +30,11 @@ defmodule DecentralisedBookIndexWeb.BookLive.Index do
<% else %> <% else %>
<div class="flex flex-wrap flex-[3_1_auto]"> <div class="flex flex-wrap flex-[3_1_auto]">
<%= for book <- @page.results do %> <%= for book <- @page.results do %>
<.book_card <.book_card book={book} current_user={@current_user} />
book={book}
current_user={@current_user}
/>
<% end %> <% end %>
</div> </div>
<.pagination <.pagination endpoint={~p"/books"} page={@page} page_params={@page_params} params={@params} />
endpoint={~p"/books"}
page={@page}
page_params={@page_params}
params={@params}
/>
<% end %> <% end %>
<.modal :if={@live_action in [:new, :edit]} id="book-modal" show on_cancel={JS.patch(~p"/books")}> <.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"> <div class="mb-4">
<%= if @book.cover_image_url != nil do %> <%= 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 %> <% else %>
<div class="relative w-36 h-36 overflow-hidden mt-14 mx-auto"> <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"> <svg
<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"/> 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> </svg>
</div> </div>
<% end %> <% 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> <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> <dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@book.description}</dd>
</dl> </dl>
<%= if not is_nil(@book.publisher) do %>
<dl> <dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Publisher</dt> <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> <dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{@book.publisher.name}
</dd>
</dl> </dl>
<% end %>
<dl> <dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Published</dt> <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> <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> <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> <dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">{@book.language}</dd>
</dl> </dl>
<%= if not Enum.empty?(@book.bids) do %>
<dl> <dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Identifiers</dt> <dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Identifiers</dt>
<%= for bid <- @book.bids do %> <%= 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> <dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
{bid.type}: {bid.bid}
</dd>
<% end %> <% end %>
</dl> </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> </div>
<%= if not Enum.empty?(@alternative_editions) do %> <%= if not Enum.empty?(@alternative_editions) do %>
<h1 class="text-lg font-semibold leading-8 text-zinc-800 dark:text-white">Editions</h1> <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]"> <div class="flex flex-wrap flex-[3_1_auto]">
<%= for book <- @alternative_editions do %> <%= for book <- @alternative_editions do %>
<.book_card <.book_card book={book} current_user={@current_user} />
book={book}
current_user={@current_user}
/>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -20,7 +20,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.FormComponent do
<.input field={@form[:name]} type="text" label="Name" /> <.input field={@form[:name]} type="text" label="Name" />
<.input field={@form[:url]} type="text" label="Url" /> <.input field={@form[:url]} type="text" label="Url" />
<% end %> <% 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[:name]} type="text" label="Name" />
<.input field={@form[:url]} type="text" label="Url" /> <.input field={@form[:url]} type="text" label="Url" />
<% end %> <% end %>
@@ -61,7 +61,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.FormComponent do
socket = socket =
socket socket
|> put_flash(:info, "Server #{socket.assigns.form.source.type}d successfully") |> 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} {:noreply, socket}
@@ -4,12 +4,15 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
alias DecentralisedBookIndex.Metadata alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.SyncTasks.SyncServerTask alias DecentralisedBookIndex.SyncTasks.SyncServerTask
alias DecentralisedBookIndex.Accounts.Role
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<.header> <.header>
Listing Servers Listing Servers
<:actions> <:actions>
<%= if @current_user != nil and Role.can_administrate?(@current_user.role) do %>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<.primary_button phx-click="sync"> <.primary_button phx-click="sync">
@@ -24,6 +27,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
</.link> </.link>
</div> </div>
</div> </div>
<% end %>
</:actions> </:actions>
</.header> </.header>
@@ -63,16 +67,13 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
<.link navigate={~p"/servers/#{dbi_server}"}>Show</.link> <.link navigate={~p"/servers/#{dbi_server}"}>Show</.link>
</div> </div>
<%= if @current_user != nil and Role.can_administrate?(@current_user.role) do %>
<.link patch={~p"/servers/#{dbi_server}/edit"}>Edit</.link> <.link patch={~p"/servers/#{dbi_server}/edit"}>Edit</.link>
<% end %>
</:action> </:action>
</.table> </.table>
<.pagination <.pagination endpoint={~p"/servers"} page={@page} page_params={@page_params} params={@params} />
endpoint={~p"/servers"}
page={@page}
page_params={@page_params}
params={@params}
/>
</div> </div>
<% end %> <% end %>
""" """
@@ -81,7 +82,10 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Index do
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
Phoenix.PubSub.subscribe(DecentralisedBookIndex.PubSub, DecentralisedBookIndex.SyncWorkerManual.topic()) Phoenix.PubSub.subscribe(
DecentralisedBookIndex.PubSub,
DecentralisedBookIndex.SyncWorkerManual.topic()
)
end end
{:ok, {:ok,
@@ -1,6 +1,8 @@
defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
use DecentralisedBookIndexWeb, :live_view use DecentralisedBookIndexWeb, :live_view
alias DecentralisedBookIndex.Accounts.Role
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@@ -8,7 +10,7 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
{@dbi_server.name} {@dbi_server.name}
<:actions> <: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()}> <.link patch={~p"/servers/#{@dbi_server}/edit"} phx-click={JS.push_focus()}>
<.edit_button> <.edit_button>
Edit Edit
@@ -18,7 +20,6 @@ defmodule DecentralisedBookIndexWeb.DbiServerLive.Show do
</:actions> </:actions>
</.header> </.header>
<div> <div>
<dl> <dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Url</dt> <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>
<dl> <dl>
<dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Sync on?</dt> <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> </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> </div>
<.back navigate={~p"/servers"}>Back to servers</.back> <.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(:page_title, page_title(socket.assigns.live_action))
|> assign( |> assign(
:dbi_server, :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 end
@@ -1,6 +1,8 @@
defmodule DecentralisedBookIndexWeb.PublisherLive.Show do defmodule DecentralisedBookIndexWeb.PublisherLive.Show do
use DecentralisedBookIndexWeb, :live_view use DecentralisedBookIndexWeb, :live_view
alias DecentralisedBookIndex.Accounts.Role
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@@ -18,6 +20,27 @@ defmodule DecentralisedBookIndexWeb.PublisherLive.Show do
</:actions> </:actions>
</.header> </.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> <.back navigate={~p"/publishers"}>Back to publishers</.back>
""" """
end end
@@ -34,7 +57,10 @@ defmodule DecentralisedBookIndexWeb.PublisherLive.Show do
|> assign(:page_title, page_title(socket.assigns.live_action)) |> assign(:page_title, page_title(socket.assigns.live_action))
|> assign( |> assign(
:publisher, :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 end
@@ -30,11 +30,7 @@ defmodule DecentralisedBookIndexWeb.UserLive.Index do
</div> </div>
<% else %> <% else %>
<div class="pt-2 flex flex-col gap-2"> <div class="pt-2 flex flex-col gap-2">
<.table <.table id="users" rows={@users} row_click={fn user -> JS.navigate(~p"/users/#{user}") end}>
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="Email">{user.email}</:col>
<:col :let={user} label="Role">{user.role}</:col> <:col :let={user} label="Role">{user.role}</:col>
@@ -48,12 +44,7 @@ defmodule DecentralisedBookIndexWeb.UserLive.Index do
</:action> </:action>
</.table> </.table>
<.pagination <.pagination endpoint={~p"/servers"} page={@page} page_params={@page_params} params={@params} />
endpoint={~p"/servers"}
page={@page}
page_params={@page_params}
params={@params}
/>
</div> </div>
<% end %> <% end %>
""" """
@@ -13,7 +13,7 @@ defmodule DecentralisedBookIndexWeb.Plugs.RequireAdmin do
conn conn
else else
conn conn
|> put_flash(:error, "Unauthorized") |> put_flash(:error, "Unauthorized!")
|> redirect(to: "/") |> redirect(to: "/")
|> halt() |> halt()
end end
+12 -10
View File
@@ -41,6 +41,18 @@ defmodule DecentralisedBookIndexWeb.Router do
scope "/", DecentralisedBookIndexWeb do scope "/", DecentralisedBookIndexWeb do
pipe_through :browser 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, ash_authentication_live_session :moderator_authenticated_routes,
on_mount: {DecentralisedBookIndexWeb.LiveUserAuth, :moderator_required} do on_mount: {DecentralisedBookIndexWeb.LiveUserAuth, :moderator_required} do
live "/books/new", BookLive.Edit, :new live "/books/new", BookLive.Edit, :new
@@ -56,19 +68,9 @@ defmodule DecentralisedBookIndexWeb.Router do
live "/publishers/:id/edit", PublisherLive.Edit, :edit live "/publishers/:id/edit", PublisherLive.Edit, :edit
live "/publishers/:id", PublisherLive.Show, :show 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", DbiServerLive.Index, :index
live "/servers/new", DbiServerLive.Edit, :new
live "/servers/:id/edit", DbiServerLive.Edit, :edit
live "/servers/:id", DbiServerLive.Show, :show 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 end
ash_authentication_live_session :maybe_authenticated_routes, ash_authentication_live_session :maybe_authenticated_routes,
+2 -1
View File
@@ -4,7 +4,7 @@ defmodule DecentralisedBookIndex.MixProject do
def project do def project do
[ [
app: :decentralised_book_index, app: :decentralised_book_index,
version: "0.1.0", version: "1.1.2",
elixir: "~> 1.14", elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
@@ -74,6 +74,7 @@ defmodule DecentralisedBookIndex.MixProject do
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"}, {:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"}, {:bandit, "~> 1.5"},
{:req, "~> 0.5.10"},
{:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false} {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false}
] ]
end end
+6 -6
View File
@@ -10,7 +10,7 @@
"assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"},
"bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"}, "bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"},
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"}, "conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
@@ -29,7 +29,7 @@
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"igniter": {:hex, :igniter, "0.5.30", "d4cfb0c523dc8536f6bdf60109a5668f7eb268189a55f84bcfbd2af0b2405538", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "f272560e81c9c85c68f693f64f239426f85d2852c157bfa0a70f235bc99c8d54"}, "igniter": {:hex, :igniter, "0.5.30", "d4cfb0c523dc8536f6bdf60109a5668f7eb268189a55f84bcfbd2af0b2405538", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "f272560e81c9c85c68f693f64f239426f85d2852c157bfa0a70f235bc99c8d54"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
@@ -38,7 +38,7 @@
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"json_xema": {:hex, :json_xema, "0.6.4", "a70ec373f54279369a0bd7c80a8ab2421fb317a68abdf2d9be71b34dbdc1ec78", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.16", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "ee91cfd6720f8ad13245fa2c438e862e6d8f46e2d700c714d72d259dcac7dfbc"}, "json_xema": {:hex, :json_xema, "0.6.4", "a70ec373f54279369a0bd7c80a8ab2421fb317a68abdf2d9be71b34dbdc1ec78", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.16", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "ee91cfd6720f8ad13245fa2c438e862e6d8f46e2d700c714d72d259dcac7dfbc"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
@@ -59,12 +59,12 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"reactor": {:hex, :reactor, "0.14.0", "8dc5d4946391010bf9fa7b58dd1e75d3c1cf97315e5489b7797cf64b82ae27a4", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9cf5068e4042791c150f0dfbc00f4f435433eb948036b44b95b940e457b35a6a"}, "reactor": {:hex, :reactor, "0.14.0", "8dc5d4946391010bf9fa7b58dd1e75d3c1cf97315e5489b7797cf64b82ae27a4", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9cf5068e4042791c150f0dfbc00f4f435433eb948036b44b95b940e457b35a6a"},
"req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"},
@@ -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 test "new author has no alternatives names", %{user: user} do
{: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 {:ok, alternatives_names} = Metadata.get_author_alternative_names(author) assert {:ok, alternatives_names} = Metadata.get_author_alternative_names(author)
assert alternatives_names = [] assert ^alternatives_names = []
end end
test "author has related author so they has one alternative name", %{user: user} do 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 {:ok, alternatives_names} = Metadata.get_author_alternative_names(author)
assert alternatives_names = [related_author] assert get_ids(alternatives_names) == [related_author.id]
end end
end end
@@ -118,7 +118,8 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
bids, bids,
author_roles, author_roles,
publisher.id, publisher.id,
nil nil,
actor: user
) )
end end
end end
@@ -146,7 +147,7 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
) )
assert {:ok, alternatives_names} = Metadata.get_book_alternative_editions(book) assert {:ok, alternatives_names} = Metadata.get_book_alternative_editions(book)
assert alternatives_names = [] assert ^alternatives_names = []
end end
test "book has related book so they has one alternative name", %{user: user} do 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 {:ok, alternatives_names} = Metadata.get_book_alternative_editions(book)
assert alternatives_names = [related_book] assert get_ids(alternatives_names) == [related_book.id]
end end
end end
@@ -218,6 +219,9 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
) )
assert {:ok, books} = Metadata.get_author_books(author) assert {:ok, books} = Metadata.get_author_books(author)
book_ids = Enum.map(books.results, & &1.id)
assert book.id in book_ids
end end
test "get the list contains aliases' books", %{user: user} do 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) 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 book.id in book_ids
assert book2.id in book_ids assert book2.id in book_ids
@@ -256,7 +260,7 @@ defmodule DecentralisedBookIndex.Metadata.BookTest do
end end
describe "get a book by book id" do describe "get a book by book id" do
test "get by bid", %{user: user} do test "get by bid" do
expected_book = generate(book()) expected_book = generate(book())
[%Metadata.BookId{type: type, bid: bid} | _] = expected_book.bids [%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 test "user can't submit" do
user = generate(user()) user = generate(user())
valid_params = %{
"name" => "Oleh",
"description" => "A cool author"
}
assert_raise Ash.Error.Forbidden, fn -> assert_raise Ash.Error.Forbidden, fn ->
form = _form =
AshPhoenix.Form.for_create(Metadata.Author, :create, AshPhoenix.Form.for_create(Metadata.Author, :create,
as: "author", as: "author",
actor: user actor: user
@@ -28,12 +23,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
end end
test "moderator can submit", %{user: user} do test "moderator can submit", %{user: user} do
valid_params = %{ assert _form =
"name" => "Oleh",
"description" => "A cool author"
}
assert form =
AshPhoenix.Form.for_create(Metadata.Author, :create, AshPhoenix.Form.for_create(Metadata.Author, :create,
as: "author", as: "author",
actor: user actor: user
@@ -55,7 +45,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
actor: user 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 end
@@ -74,7 +64,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.AuthorFormTest do
actor: user 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 end
end end
@@ -4,7 +4,6 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
import ExUnitProperties import ExUnitProperties
alias DecentralisedBookIndex.Metadata alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.Metadata.Book
setup do setup do
user = generate(user(role: :moderator)) user = generate(user(role: :moderator))
@@ -14,22 +13,9 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
describe "form submiting" do describe "form submiting" do
test "user can't submit" do test "user can't submit" do
user = generate(user()) 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 -> assert_raise Ash.Error.Forbidden, fn ->
form = _form =
AshPhoenix.Form.for_create(Metadata.Book, :create, AshPhoenix.Form.for_create(Metadata.Book, :create,
as: "book", as: "book",
actor: user actor: user
@@ -39,21 +25,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
end end
test "moderator can submit", %{user: user} do test "moderator can submit", %{user: user} do
bids = bids(actor: user) assert _form =
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 =
AshPhoenix.Form.for_create(Metadata.Book, :create, AshPhoenix.Form.for_create(Metadata.Book, :create,
as: "book", as: "book",
actor: user actor: user
@@ -86,7 +58,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
actor: user actor: user
) )
assert {:ok, user} = AshPhoenix.Form.submit(form, params: valid_params) assert {:ok, _book} = AshPhoenix.Form.submit(form, params: valid_params)
end end
end end
@@ -106,7 +78,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
actor: user actor: user
) )
assert {:ok, user} = AshPhoenix.Form.submit(form, params: valid_params) assert {:ok, _book} = AshPhoenix.Form.submit(form, params: valid_params)
end end
test "update bids and authors", %{user: user} do test "update bids and authors", %{user: user} do
@@ -125,7 +97,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.BookFormTest do
actor: user actor: user
) )
assert {:ok, user} = AshPhoenix.Form.submit(form, params: valid_params) assert {:ok, _book} = AshPhoenix.Form.submit(form, params: valid_params)
end end
end end
@@ -12,12 +12,8 @@ defmodule DecentralisedBookIndex.Metadata.Forms.PublisherFormTest do
test "user can't submit" do test "user can't submit" do
user = generate(user()) user = generate(user())
valid_params = %{
"name" => "Publisher"
}
assert_raise Ash.Error.Forbidden, fn -> assert_raise Ash.Error.Forbidden, fn ->
form = _form =
AshPhoenix.Form.for_create(Metadata.Publisher, :create, AshPhoenix.Form.for_create(Metadata.Publisher, :create,
as: "publisher", as: "publisher",
actor: user actor: user
@@ -27,11 +23,7 @@ defmodule DecentralisedBookIndex.Metadata.Forms.PublisherFormTest do
end end
test "moderator can submit", %{user: user} do test "moderator can submit", %{user: user} do
valid_params = %{ assert _form =
"name" => "Publisher"
}
assert form =
AshPhoenix.Form.for_create(Metadata.Publisher, :create, AshPhoenix.Form.for_create(Metadata.Publisher, :create,
as: "publisher", as: "publisher",
actor: user actor: user
@@ -81,5 +81,40 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorTransformerTest do
updated_at: "2025-03-26T10:16:16.124581Z" updated_at: "2025-03-26T10:16:16.124581Z"
} = author } = author
end 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
end end
@@ -138,5 +138,70 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookTransformerTest do
dbi_server_id: "889a323e-d104-4b5d-b276-dad5a9b1da99" dbi_server_id: "889a323e-d104-4b5d-b276-dad5a9b1da99"
} = book } = book
end 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
end end
@@ -8,6 +8,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
json_body = %{ json_body = %{
"data" => %{ "data" => %{
"attributes" => %{ "attributes" => %{
"dbi_server_id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
"inserted_at" => "2025-03-22T20:07:30.766249Z", "inserted_at" => "2025-03-22T20:07:30.766249Z",
"name" => "Test", "name" => "Test",
"updated_at" => "2025-05-04T18:48:44.213309Z", "updated_at" => "2025-05-04T18:48:44.213309Z",
@@ -16,7 +17,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
"id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc", "id" => "0c0647ec-07ef-4caa-b683-5847dbfbe5cc",
"links" => %{}, "links" => %{},
"meta" => %{}, "meta" => %{},
"relationships" => %{}, "relationships" => %{"dbi_server" => %{"links" => %{}, "meta" => %{}}},
"type" => "dbi_server" "type" => "dbi_server"
}, },
"jsonapi" => %{"version" => "1.0"}, "jsonapi" => %{"version" => "1.0"},
@@ -34,11 +35,40 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerTransformerTest
name: "Test", name: "Test",
url: "http://localhost:4001", url: "http://localhost:4001",
inserted_at: "2025-03-22T20:07:30.766249Z", 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 } = server
end end
test "a json doesn't contains server information \"data\" attribute" do 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 = %{ json_body = %{
"attributes" => %{ "attributes" => %{
"inserted_at" => "2025-03-22T20:07:30.766249Z", "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", inserted_at: "2025-03-22T20:07:30.766249Z",
updated_at: "2025-05-04T18:48:44.213309Z" updated_at: "2025-05-04T18:48:44.213309Z"
} = server } = server
refute Map.has_key?(server, :dbi_server_id)
end end
end end
end end
@@ -63,5 +63,32 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.PublisherTransformerTest
dbi_server_id: "889a323e-d104-4b5d-b276-dad5a9b1da99" dbi_server_id: "889a323e-d104-4b5d-b276-dad5a9b1da99"
} = publisher } = publisher
end 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
end end
@@ -26,17 +26,13 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.AuthorSyncTest do
updated_at: "2025-03-21T09:20:48.791539Z" 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 = AuthorSync.create_update(author, server.id)
assert {:ok, saved_author} = Metadata.get_author_by_id(author.id) assert {:ok, saved_author} = Metadata.get_author_by_id(author.id)
author = author =
author author
|> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_at)
|> Map.delete(:author_alias_registry_id) |> Map.delete(:author_alias_registry_id)
|> datetime_from_iso8601_for_map()
assert get_submap(saved_author, author) == author assert get_submap(saved_author, author) == author
assert nil != saved_author.author_alias_registry_id 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" 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 = AuthorSync.create_update(author, server.id)
assert {:ok, saved_author} = Metadata.get_author_by_id(author.id) assert {:ok, saved_author} = Metadata.get_author_by_id(author.id)
author = author =
author author
|> Map.replace(:inserted_at, inserted_at) |> datetime_from_iso8601_for_map()
|> Map.replace(:updated_at, updated_at)
assert get_submap(saved_author, author) == author assert get_submap(saved_author, author) == author
assert nil != saved_author.author_alias_registry_id 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 = AuthorSync.create_update(author_attrs, server.id)
assert {:ok, saved_author} = Metadata.get_author_by_id(author.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 end
test "the local existing author wouldn't updated", %{user: user} do 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" 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 = BookSync.create_update(book, server.id)
assert {:ok, saved_book} = Metadata.get_book_by_id(book.id) assert {:ok, saved_book} = Metadata.get_book_by_id(book.id)
book = book =
book book
|> Map.replace(:inserted_at, inserted_at) |> datetime_from_iso8601_for_map()
|> Map.replace(:updated_at, updated_at)
|> Map.replace(:published, Date.from_iso8601!(book[:published])) |> Map.replace(:published, Date.from_iso8601!(book[:published]))
assert nil != saved_book.book_editions_registry_id assert nil != saved_book.book_editions_registry_id
assert server.id == saved_book.dbi_server_id assert server.id == saved_book.dbi_server_id
assert get_submap(saved_book, book) == book
end end
test "a new book has the same editions registry" do 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" 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 = BookSync.create_update(book, server.id)
assert {:ok, saved_book} = Metadata.get_book_by_id(book.id) assert {:ok, saved_book} = Metadata.get_book_by_id(book.id)
book = book =
book book
|> Map.replace(:inserted_at, inserted_at) |> datetime_from_iso8601_for_map()
|> Map.replace(:updated_at, updated_at)
|> Map.replace(:published, Date.from_iso8601!(book[:published])) |> Map.replace(:published, Date.from_iso8601!(book[:published]))
assert nil != saved_book.book_editions_registry_id assert nil != saved_book.book_editions_registry_id
assert server.id == saved_book.dbi_server_id assert server.id == saved_book.dbi_server_id
assert get_submap(saved_book, book) == book
end end
test "a new book with relationships will be created" do 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 = 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 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)) server = generate(dbi_server(url: @test_server_endpoint))
book = generate(book()) book = generate(book())
@@ -195,7 +189,7 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.BookSyncTest do
assert saved_book.dbi_server_id == nil assert saved_book.dbi_server_id == nil
end 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)) server = generate(dbi_server(url: @test_server_endpoint))
book = generate(book()) 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) assert Enum.all?(old_roles, fn id -> id not in new_roles end)
end 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 end
@@ -35,14 +35,15 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerSyncTest do
|> Map.replace(:inserted_at, inserted_at) |> Map.replace(:inserted_at, inserted_at)
|> Map.replace(:updated_at, updated_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 assert server.id == saved_dbi_server.dbi_server_id
end end
test "update an existing dbi_server", %{user: user} do test "update an existing dbi_server", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint)) 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 = %{ dbi_server_attrs = %{
id: dbi_server.id, id: dbi_server.id,
@@ -55,7 +56,9 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.DBIServerSyncTest do
assert :ok = DBIServerSync.create_update(dbi_server_attrs, server.id) 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 {: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 assert server.id == saved_dbi_server.dbi_server_id
end end
end end
@@ -23,9 +23,6 @@ defmodule DecentralisedBookIndex.Sync.DataTransformers.PublisherSyncTest do
updated_at: "2025-03-21T09:20:48.791539Z" 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 = PublisherSync.create_update(publisher, server.id)
assert {:ok, saved_publisher} = Metadata.get_publisher_by_id(publisher.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 test "sync authors", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint)) 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)
{:ok, author} = Metadata.create_author("Author2", "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 end
end end
@@ -2,7 +2,6 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncBookTaskTest do
use DecentralisedBookIndex.DataCase use DecentralisedBookIndex.DataCase
alias DecentralisedBookIndex.SyncTasks.SyncBooksTask alias DecentralisedBookIndex.SyncTasks.SyncBooksTask
alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.TestEndpoints alias DecentralisedBookIndex.TestEndpoints
@test_server_endpoint TestEndpoints.test_api_endpoint() @test_server_endpoint TestEndpoints.test_api_endpoint()
@@ -14,7 +13,7 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncBookTaskTest do
_book = generate(book()) _book = generate(book())
_book = generate(book()) _book = generate(book())
assert server = SyncBooksTask.sync(server) assert ^server = SyncBooksTask.sync(server)
end end
end 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("Test", "http://localhost:4001", false, actor: user)
{:ok, _dbi_server} = Metadata.create_dbi_server("Test2", "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 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("Publisher", actor: user)
{:ok, _publisher} = Metadata.create_publisher("Publisher2", actor: user) {:ok, _publisher} = Metadata.create_publisher("Publisher2", actor: user)
assert server = SyncPublishersTask.sync(server) assert ^server = SyncPublishersTask.sync(server)
end end
end end
end end
@@ -3,9 +3,6 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncServerTaskTest do
alias DecentralisedBookIndex.SyncTasks.SyncServerTask alias DecentralisedBookIndex.SyncTasks.SyncServerTask
alias DecentralisedBookIndex.Sync.ApiClients.FetchJson
alias DecentralisedBookIndex.Sync.AuthorSync
alias DecentralisedBookIndex.Metadata alias DecentralisedBookIndex.Metadata
alias DecentralisedBookIndex.TestEndpoints alias DecentralisedBookIndex.TestEndpoints
@@ -18,19 +15,19 @@ defmodule DecentralisedBookIndex.SyncTasks.SyncServerTaskTest do
describe "sync all" do describe "sync all" do
test "servers", %{user: user} do test "servers", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint)) _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_all() assert :ok = SyncServerTask.sync_all()
end end
end end
describe "sync one" do describe "sync one" do
test "server", %{user: user} do test "server", %{user: user} do
server = generate(dbi_server(url: @test_server_endpoint)) 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 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
+13 -6
View File
@@ -35,13 +35,16 @@ defmodule DecentralisedBookIndex.Generators do
generate(user(role: :moderator)) generate(user(role: :moderator))
end) end)
dbi_server_id = opts[:dbi_server_id] || nil
changeset_generator( changeset_generator(
Metadata.Author, Metadata.Author,
:create, :create,
defaults: [ defaults: [
name: sequence(:name, &"Author #{&1}"), name: sequence(:name, &"Author #{&1}"),
description: sequence(:name, &"Description #{&1}"), description: sequence(:name, &"Description #{&1}"),
author_alias_registry_id: nil author_alias_registry_id: nil,
dbi_server_id: dbi_server_id
], ],
overrides: opts, overrides: opts,
actor: actor actor: actor
@@ -83,16 +86,17 @@ defmodule DecentralisedBookIndex.Generators do
url = url =
opts[:url] || opts[:url] ||
once(:default_actor, fn ->
"http://localhost:4002" "http://localhost:4002"
end)
dbi_server_id = opts[:dbi_server_id] || nil
changeset_generator( changeset_generator(
Metadata.DBIServer, Metadata.DBIServer,
:create, :create,
defaults: [ defaults: [
name: sequence(:name, &"Server #{&1}"), name: sequence(:name, &"Server #{&1}"),
url: url url: url,
dbi_server_id: dbi_server_id
], ],
overrides: opts, overrides: opts,
actor: actor actor: actor
@@ -115,7 +119,7 @@ defmodule DecentralisedBookIndex.Generators do
count = count =
opts[:count] || 2 opts[:count] || 2
types = ["isbn10", "isbn13", "asin"] types = ["isbn", "isbn13", "asin"]
if count > length(types), do: count = length(types) if count > length(types), do: count = length(types)
@@ -136,11 +140,14 @@ defmodule DecentralisedBookIndex.Generators do
generate(user(role: :moderator)) generate(user(role: :moderator))
end) end)
dbi_server_id = opts[:dbi_server_id] || nil
changeset_generator( changeset_generator(
Metadata.Publisher, Metadata.Publisher,
:create, :create,
defaults: [ defaults: [
name: sequence(:name, &"Publisher #{&1}") name: sequence(:name, &"Publisher #{&1}"),
dbi_server_id: dbi_server_id
], ],
overrides: opts, overrides: opts,
actor: actor actor: actor
+18
View File
@@ -40,4 +40,22 @@ defmodule DecentralisedBookIndex.Helpers do
|> DateTime.add(seconds) |> DateTime.add(seconds)
|> DateTime.to_iso8601() |> DateTime.to_iso8601()
end 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 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