Compare commits

...

63 Commits

Author SHA1 Message Date
5dfbf7011e Add basic functions for keeping track of Deepl account usage 2023-03-15 11:35:26 +01:00
2eba3bc500 Update Translators context 2023-03-15 11:26:07 +01:00
43f3ea193f Add miscellaneous stuff 2023-03-15 10:56:30 +01:00
61253f301a Add mainly disfunctional test
Due to utter negligence test results are currently:

221 tests, 186 failures
2023-03-15 10:49:29 +01:00
5d43e61223 Update status styles 2023-03-08 21:09:12 +01:00
724d161f50 Add status preview in translation_live/show 2023-03-08 20:43:42 +01:00
1fb9a40f2c Add overlooked ids for buttons 2023-03-06 23:01:31 +01:00
459c8e6a57 Add shortcuts for saving 2023-03-05 23:13:58 +01:00
21b97bec6e Sanitize variable name 2023-03-05 22:53:52 +01:00
5d9238a65a Fix ugly bug and add more shortcuts 2023-03-05 22:05:54 +01:00
e20f8e33ee Add selecting next/previous tunit and highlight it 2023-03-05 20:56:45 +01:00
4e6c516cb6 Add Public context to iex-local.exs 2023-03-05 20:42:51 +01:00
bacb61252f Adapt core_components table() to dark mode 2023-03-04 23:46:39 +01:00
8a513b1452 Remove unused code 2023-03-04 23:45:49 +01:00
895860baa6 Update Public.get_autor! to only display public Artikel
Also get rid of the superfluous additional loop over articles/translations.
2023-03-03 22:05:52 +01:00
5319785855 Update continue_edit() to reload Translation
Reloading is necessary to detect changes in the changeset. Otherwise
simple changes like changing the value of translation.public wouldn't
get noticed and not being saved to db.
2023-03-03 21:57:09 +01:00
b20bbb232c Refactor query functions 2023-03-02 23:17:21 +01:00
cbea9450e4 Add Autor schema 2023-03-02 23:13:32 +01:00
3fe4a331ac Remove detour over Enum.into(%{}) for keyword results to %Artikel{} 2023-03-01 16:01:14 +01:00
6d0ae825d8 Remove tids for Translations 2023-03-01 15:58:38 +01:00
3a2769eed1 Refactor Autoren context into Public context 2023-03-01 15:24:36 +01:00
e8089eb24e Add important amendment
Yesterday night it got late...
2023-03-01 12:41:09 +01:00
02e6340c0a Add embedded schema for Artikel 2023-02-28 23:47:37 +01:00
ed98f4cbc4 Add some styling 2023-02-28 21:45:04 +01:00
b87b54ec71 Refactor get_artikel*() functions/sql 2023-02-28 21:42:05 +01:00
fc0818678c Add dark mode styling to form elements 2023-02-28 21:38:11 +01:00
dc4c8e4790 Update tidy_raw() to ignore null values 2023-02-28 21:35:09 +01:00
e98187200a Update to Phoenix 1.7.0 final release 2023-02-27 19:47:13 +01:00
b47d7d081d Add .iex-local.exs 2023-02-23 15:46:49 +01:00
2ffea3e490 Add rest of other commit (appr. HEAD~20?) 2023-02-22 14:24:49 +01:00
cb8e9ef14f Add another tidy_raw() 2023-02-14 22:52:43 +01:00
faf2bb0e2e Improve logic and add working "delete" links 2023-02-14 21:02:48 +01:00
7297a907da Update regexes with u option
And add pw validations
2023-02-14 20:56:57 +01:00
66a61c8380 Add tidy_raw() helper 2023-02-14 20:55:36 +01:00
b0267ef752 Add long urls for Artikel 2023-02-12 18:46:19 +01:00
239177db50 Add makeshift image for dark mode 2023-02-11 23:24:19 +01:00
1b68a1de16 Add some forgotten code 2023-02-11 20:12:03 +01:00
6ae1289400 More styling 2023-02-11 20:05:40 +01:00
666c499c72 Add .breakpoint_indicator 2023-02-11 20:05:00 +01:00
7cd9a09cfd More dark mode 2023-02-11 19:56:34 +01:00
32df023891 Update deps 2023-02-11 15:46:43 +01:00
38000db27a Add dark mode styling 2023-02-11 15:46:11 +01:00
8f2698bf7b Amend dark mode related 2023-02-06 16:15:04 +01:00
ba2949a3bd Add eventlistener for change of day/night mode
And add a static js folder for js-files apart from the asset pipeline.
2023-02-05 20:14:06 +01:00
ab2e8ae816 Add dark mode 2023-02-04 23:10:57 +01:00
4cb07692b1 Update styles 2023-01-31 18:22:05 +01:00
c5853fc2aa Add configurable prevention of user registration
Should be done appropriately soon.
2023-01-31 18:16:28 +01:00
54b609185d Fix issue in a botchy way 2023-01-31 18:12:39 +01:00
119ef28746 Add top image 2023-01-31 18:10:18 +01:00
4d75b598ff Update design of public pages 2023-01-31 18:08:10 +01:00
6cafe09331 Update default route 2023-01-31 18:03:58 +01:00
98426773b7 Add layout for public pages 2023-01-31 18:02:10 +01:00
60eaca943a Cleanup superfluous code 2023-01-30 22:05:38 +01:00
1578d9932a Add tz timezone database 2023-01-27 14:15:01 +01:00
f76f218652 Sanitize HtmlTreeComponent 2023-01-27 12:02:29 +01:00
13c08918cc Add treating <img> as block and allow <a> in both contexts 2023-01-27 11:30:56 +01:00
38b3f0c272 Add hyphenation and a generalized render_public_content function 2023-01-26 21:48:50 +01:00
e8e7f877e7 Add beauty 2023-01-24 20:58:38 +01:00
0089d22da4 Fix issue with "Save and edit" 2023-01-23 15:40:05 +01:00
2db0ff06ac Fix perennial :atom issue 2023-01-23 15:37:47 +01:00
0a10a5c12c Add more styles 2023-01-23 15:34:53 +01:00
615e64cbd7 Fix issue with sometimes missing seconds_remaining field in Deepl response 2023-01-23 15:34:26 +01:00
bec0e0ae5c Add exporting directly to a file 2023-01-23 14:30:25 +01:00
77 changed files with 1220 additions and 351 deletions

22
.iex-local.exs Normal file
View File

@ -0,0 +1,22 @@
alias Outlook.HtmlPreparations
alias Outlook.HtmlPreparations.HtmlPreparation
alias Outlook.InternalTree.{Html,InternalNode,TranslationUnit}
alias Outlook.InternalTree
alias Outlook.Articles
alias Outlook.Accounts
alias Outlook.Articles.Article
alias Outlook.Authors
alias Outlook.Authors.Author
alias Outlook.Translations
alias Outlook.Translations.Translation
alias Outlook.Translators.{Deepl,DeeplAccount}
alias Outlook.Translators
alias Outlook.Public
alias Outlook.Public.{Artikel,Autor}
alias Outlook.Repo
import Ecto.Query, warn: false
html = """
<p class="">Das Young-Global-Leaders-Programm des WEF von Klaus Schwab tut seit Anfang der 90er Jahre das gleiche und wäre es ein Konkurrenzprodukt gegen die bestehenden, transatlantischen US-Programme, wäre das Projekt sofort abgewürgt worden. Stattdessen ist es ein großer Erfolg und sehr viele der heute weltweit führenden Politiker sind durch die Schule von Klaus Schwab gegangen und setzen als Minister und sogar Regierungschefs brav die Politik um, für die sich Schwab selbst einsetzt.</p>
<p>Die Frage ist also, wie und mit wessen Hilfe es der aus kleinen Verhältnissen stammende Klaus Schwab geschafft hat, so mächtig zu werden. Die Antwort ist verblüffend einfach: Er hat als Student selbst so ein Programm durchlaufen. Damals war es noch die CIA, die relativ offen hinter diesem Programm stand und junge Leute gesucht hat, deren Karriere die CIA gefördert hat, damit diese Leute später das umsetzen, was von der CIA gewollt ist. Inzwischen hat Schwab mit seinem WEF diese Funktion übernommen und sein Young-Global-Leaders-Programm ist nichts anderes, als das Nachfolgeprogramm eines CIA-Programms aus den 1950er Jahren.</p>
<p>Auf den Artikel bin ich durch einen Hinweis eines Lesers auf einen <a rel="noreferrer noopener" href="https://tkp.at/2022/09/02/das-young-global-leaders-programm-des-wef-und-sein-ursprung-in-den-usa/" target="_blank">Artikel bei tkp </a>gestoßen, der den englischen Artikel zusammengefasst hat. Da die Informationen im Original so spannend und die Details zum Verständnis so wichtig sind, habe ich den <a rel="noreferrer noopener" href="https://unlimitedhangout.com/2022/08/investigative-reports/the-kissinger-continuum-the-unauthorized-history-of-the-wefs-young-global-leaders-program/" target="_blank" class="">englischen Originalartikel</a> übersetzt, um mich nicht mit fremden Federn zu schmücken. Die Links habe ich aus dem Originalartikel übernommen.</p>
"""

View File

@ -1,11 +1,47 @@
.article { .main {
@apply pr-8 @apply place-content-center;
} }
.article .tunit { .article {
/* @apply pr-8 */
max-width: 25rem;
}
.article span.tunit {
@apply hover:bg-gray-300; @apply hover:bg-gray-300;
} }
.dark .article span.tunit {
@apply hover:bg-gray-700;
}
.article span.tunit[current="yes"] {
@apply bg-amber-300 text-stone-700 hover:bg-amber-200 hover:text-red-900;
}
.dark .article span.tunit[current="yes"] {
@apply bg-amber-500/70 text-white hover:bg-amber-500/70 hover:text-red-900;
}
.article.show_status span.tunit a {
@apply text-inherit;
}
.article.show_status span.tunit[status="untranslated"] {
@apply text-red-900;
}
.article.show_status span.tunit[status="passable"] {
@apply text-amber-500;
}
.dark .article.show_status span.tunit[status="untranslated"] {
@apply text-red-500;
}
.dark .article.show_status span.tunit[status="passable"] {
@apply text-amber-100;
}
.article a.hide-link { .article a.hide-link {
display: none; display: none;
} }
@ -27,7 +63,11 @@
} }
.article h4 { .article h4 {
@apply font-semibold text-lg leading-8 text-zinc-800; @apply my-2 font-semibold text-lg leading-8 text-stone-800;
}
.dark .article h4 {
@apply text-stone-300;
} }
.article p, .article div { .article p, .article div {
@ -45,3 +85,29 @@
.article a { .article a {
@apply text-cyan-900; @apply text-cyan-900;
} }
.dark .article a {
@apply text-cyan-700;
}
.article ul {
@apply pl-6 list-disc my-2;
}
.article ol {
@apply pl-8 list-decimal my-2;
}
.article li {
@apply text-justify;
}
.article img {
max-width: 100%;
height: auto;
}
.article img + div, a + div {
font-size: smaller;
margin-bottom: 0.6ex;
}

View File

@ -22,8 +22,16 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view" import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
import {DarkModeHook} from './dark-mode-widget'
import {TranslationFormHook} from "./translation-form"
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, {params: {_csrf_token: csrfToken}}) let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: {translation_form: TranslationFormHook,
// tunit_editor: TunitEditorHook,
dark_mode_widget: DarkModeHook},
})
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
@ -38,4 +46,3 @@ liveSocket.connect()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket

View File

@ -0,0 +1,40 @@
let DarkModeHook = {
mounted() {
this.button = this.el.querySelector("button")
this.button.addEventListener("click", this.toggle_chooser.bind(this))
this.chooser = this.el.querySelector("ul")
var lis = this.el.querySelectorAll("li")
lis[0].addEventListener("click", this.switch_to_day_mode.bind(this))
lis[1].addEventListener("click", this.switch_to_night_mode.bind(this))
lis[2].addEventListener("click", this.switch_to_system_mode.bind(this))
},
toggle_chooser(){
if (this.chooser.classList.contains("hidden")){
this.chooser.classList.remove("hidden")
} else {
this.chooser.classList.add("hidden")
}
},
switch_to_day_mode() {
document.documentElement.classList.remove('dark')
localStorage.theme = 'light'
this.chooser.classList.add("hidden")
},
switch_to_night_mode() {
document.documentElement.classList.add('dark')
localStorage.theme = 'dark'
this.chooser.classList.add("hidden")
},
switch_to_system_mode() {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
localStorage.removeItem('theme')
this.chooser.classList.add("hidden")
},
}
export {DarkModeHook}

View File

@ -0,0 +1,52 @@
let TranslationFormHook = {
mounted() {
this.el.addEventListener("keyup", this.keyupHandler.bind(this))
this.title_input = this.el.querySelector("#translation-form_title")
this.tunit_editor = this.el.querySelector("#tunit-editor-content")
this.save_edit_button = this.el.querySelector("#save-edit-button")
this.save_publish_button = this.el.querySelector("#save-publish-button")
},
keyupHandler(e) {
var push_event = true
var preaction = () => { }
var postaction = () => { }
var payload = {}
if (e.altKey){
if (e.ctrlKey){
if (e.key == "s"){
this.save_edit_button.click()
} else if (e.key == "p"){
this.save_publish_button.click()
}
} else {
if (e.key == "ArrowDown" || e.key == "n"){
preaction = () => { this.title_input.focus() }
postaction = () => { this.tunit_editor.focus() }
var handler = "select_next_tunit"
} else if (e.key == "ArrowUp" || e.key == "v"){
preaction = () => { this.title_input.focus() }
postaction = () => { this.tunit_editor.focus() }
var handler = "select_previous_tunit"
} else if (e.key == "u") {
var handler = "tunit_status"
payload = {status: "untranslated"}
} else if (e.key == "p") {
var handler = "tunit_status"
payload = {status: "passable"}
} else if (e.key == "o") {
var handler = "tunit_status"
payload = {status: "done"}
} else {
push_event = false
}
if (push_event) {
preaction.call()
this.pushEventTo(this.el, handler, payload, postaction)
}
}
}
},
}
export {TranslationFormHook}

View File

@ -4,6 +4,7 @@
const plugin = require("tailwindcss/plugin") const plugin = require("tailwindcss/plugin")
module.exports = { module.exports = {
darkMode: 'class',
content: [ content: [
"./js/**/*.js", "./js/**/*.js",
"../lib/*_web.ex", "../lib/*_web.ex",
@ -23,4 +24,4 @@ module.exports = {
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])) plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]))
] ]
} }

View File

@ -131,3 +131,5 @@ config :floki, :html_parser, Floki.HTMLParser.FastHtml
config :nanoid, config :nanoid,
size: 12, size: 12,
alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
config :elixir, :time_zone_database, Tz.TimeZoneDatabase

View File

@ -45,7 +45,7 @@ defmodule Outlook.Accounts.User do
defp validate_email(changeset, opts) do defp validate_email(changeset, opts) do
changeset changeset
|> validate_required([:email]) |> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/u, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160) |> validate_length(:email, max: 160)
|> maybe_validate_unique_email(opts) |> maybe_validate_unique_email(opts)
end end
@ -53,10 +53,10 @@ defmodule Outlook.Accounts.User do
defp validate_password(changeset, opts) do defp validate_password(changeset, opts) do
changeset changeset
|> validate_required([:password]) |> validate_required([:password])
|> validate_length(:password, min: 12, max: 72) |> validate_length(:password, min: 8, max: 72)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character") |> validate_format(:password, ~r/[a-z]/u, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character") |> validate_format(:password, ~r/[A-Z]/u, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/u, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts) |> maybe_hash_password(opts)
end end

View File

@ -14,7 +14,7 @@ defmodule Outlook.Articles.InternalTree do
def cast(_), do: :error def cast(_), do: :error
def load(tree) when is_binary(tree) do def load(tree) when is_binary(tree) do
{:ok, Jason.decode!(tree, keys: :atoms!) |> from_json} {:ok, Jason.decode!(tree, keys: :atoms) |> from_json}
end end
def dump(tree) when is_list(tree) do def dump(tree) when is_list(tree) do

View File

@ -1,21 +0,0 @@
defmodule Outlook.Artikel do
@moduledoc """
The Artikel context.
"""
alias Outlook.Translations.Translation
import Ecto.Query, warn: false
alias Outlook.Repo
def list_artikel do
Repo.all(from t in Translation, where: t.public == true)
|> Repo.preload([article: :author])
end
def get_artikel!(artikel) when is_struct(artikel), do: get_artikel!(artikel.id)
def get_artikel!(id) do
Repo.one(from t in Translation, where: t.id == ^id and t.public == true)
|> Repo.preload([article: :author])
end
end

View File

@ -39,7 +39,7 @@ defmodule Outlook.Authors do
def get_author_with_articles!(id) do def get_author_with_articles!(id) do
Repo.get!(Author, id) Repo.get!(Author, id)
|> Repo.preload([:articles]) |> Repo.preload([articles: :translations])
end end
@doc """ @doc """

View File

@ -9,7 +9,7 @@ defmodule Outlook.Authors.Author do
field :homepage_name, :string field :homepage_name, :string
field :homepage_url, :string field :homepage_url, :string
field :name, :string field :name, :string
has_many :articles, Article has_many :articles, Article, on_delete: :delete_all
timestamps() timestamps()
end end

View File

@ -1,34 +0,0 @@
defmodule Outlook.Autoren do
@moduledoc """
The Autoren context.
"""
import Ecto.Query, warn: false
alias Outlook.Repo
alias Outlook.Articles.Article
alias Outlook.Translations.Translation
alias Outlook.Authors.Author
def list_autoren do
Repo.all(Author)
end
def get_autor!(id) do
Repo.get!(Author, id)
|> Repo.preload([articles: [:translations]])
end
@doc "This is ugly"
def list_artikel(author) when is_struct(author), do: list_artikel(author.id)
def list_artikel(author_id) do
aids = Repo.all(from a in Article,
select: [:id],
where: a.author_id == ^author_id)
|> Enum.map(fn a -> a.id end)
Repo.all(from t in Translation,
select: [t.title, t.teaser, t.date, t.user_id],
where: t.article_id in ^aids and t.public == true)
end
end

View File

@ -11,10 +11,4 @@ defmodule Outlook.HtmlPreparations do
|> HtmlPreparation.floki_to_internal |> HtmlPreparation.floki_to_internal
|> HtmlPreparation.set_sibling_with |> HtmlPreparation.set_sibling_with
end end
def get_tree_items(content_tree) do
content_tree
|> HtmlPreparation.strip_whitespace_textnodes
|> HtmlPreparation.build_indentation_list(0)
end
end end

View File

@ -3,8 +3,9 @@ defmodule Outlook.HtmlPreparations.HtmlPreparation do
alias Outlook.InternalTree.InternalNode alias Outlook.InternalTree.InternalNode
@block_elements ["address","article","aside","blockquote","canvas","dd","div","dl","dt","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hr","li","main","nav","noscript","ol","p","pre","section","table","tfoot","ul","video"] # treating img as block element because inline images are not desirable
# @inline_elements ["a","abbr","acronym","b","bdo","big","br","button","cite","code","dfn","em","i","img","input","kbd","label","map","object","output","q","samp","script","select","small","span","strong","sub","sup","textarea","time","tt","u","var"] @block_elements ~w(img address article aside blockquote canvas dd div dl dt fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr li main nav noscript ol p pre section table tfoot ul video)
# @inline_elements ~w(a abbr acronym b bdo big br button cite code dfn em i input kbd label map object output q samp script select small span strong sub sup textarea time tt u var)
defp clean_atts_to_map(atts) do defp clean_atts_to_map(atts) do
atts_to_keep = ~w(href src) atts_to_keep = ~w(href src)
@ -43,9 +44,16 @@ defmodule Outlook.HtmlPreparations.HtmlPreparation do
} | floki_to_internal(rest) ] } | floki_to_internal(rest) ]
end end
def floki_to_internal([ ]), do: ( [ ] ) def floki_to_internal([]), do: []
def set_sibling_with([ node | rest ]) when node.name == "a" do
[ %InternalNode{ node |
eph: %{sibling_with: :both}, # <a> may occur at block level (e.g. when enclosing an <img>)
content: set_sibling_with(node.content)
} | set_sibling_with(rest) ]
end
def set_sibling_with([ %{type: :element} = node | rest ]) do def set_sibling_with([ %{type: :element} = node | rest ]) do
[ %InternalNode{ node | [ %InternalNode{ node |
eph: %{sibling_with: node.name in @block_elements && :block || :inline}, eph: %{sibling_with: node.name in @block_elements && :block || :inline},
@ -55,17 +63,17 @@ defmodule Outlook.HtmlPreparations.HtmlPreparation do
def set_sibling_with([ node | rest ]) do def set_sibling_with([ node | rest ]) do
sib_with = case node.type do sib_with = case node.type do
:text -> Regex.match?(~r/^\s*$/, node.content) && :both || :inline :text -> Regex.match?(~r/^\s*$/u, node.content) && :both || :inline
:comment -> :both :comment -> :both
end end
[ %InternalNode{ node | eph: %{sibling_with: sib_with} } | set_sibling_with(rest) ] [ %InternalNode{ node | eph: %{sibling_with: sib_with} } | set_sibling_with(rest) ]
end end
def set_sibling_with([ ]), do: ( [ ] ) def set_sibling_with([]), do: []
def strip_whitespace_textnodes [ %{type: :text} = node | rest] do def strip_whitespace_textnodes [ %{type: :text} = node | rest] do
if Regex.match?(~r/^\s*$/, node.content) do if Regex.match?(~r/^\s*$/u, node.content) do
strip_whitespace_textnodes(rest) strip_whitespace_textnodes(rest)
else else
[ node | strip_whitespace_textnodes(rest)] [ node | strip_whitespace_textnodes(rest)]
@ -82,20 +90,4 @@ defmodule Outlook.HtmlPreparations.HtmlPreparation do
end end
def strip_whitespace_textnodes([]), do: [] def strip_whitespace_textnodes([]), do: []
def build_indentation_list [ %{type: :element} = node | rest], level do
[ %{node: Map.replace(node, :content, []), level: level}
| [ build_indentation_list(node.content, level + 1)
| build_indentation_list(rest, level)
]
] |> List.flatten
end
def build_indentation_list [ node | rest ], level do
[ %{node: node, level: level}
| build_indentation_list( rest, level ) ]
end
def build_indentation_list([ ], _), do: []
end end

View File

@ -0,0 +1,36 @@
defmodule Outlook.Hyphenation do
def hyphenate(html, lang) do
form = get_multipart_form(
[
{"api-token", System.get_env("HYPH_API_TOKEN")},
{"hyph[lang]", String.downcase(lang)},
{"hyph[text]", html},
]
)
case HTTPoison.request(
:post,
System.get_env("HYPH_URL"),
form,
get_multipart_headers()
) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
body
_ ->
# this is poor but for now better than loss of all work
html
end
end
defp get_multipart_form fields do
{:multipart, fields}
end
defp get_multipart_headers() do
[
"Content-Type": "multipart/form-data",
"Transfer-Encoding": "chunked",
]
end
end

View File

@ -2,6 +2,7 @@ defmodule Outlook.InternalTree do
alias Outlook.InternalTree.{Html,Modifiers,RawInternalBasic,InternalTree,Translation} alias Outlook.InternalTree.{Html,Modifiers,RawInternalBasic,InternalTree,Translation}
alias Outlook.HtmlPreparations.HtmlPreparation alias Outlook.HtmlPreparations.HtmlPreparation
alias Outlook.{Hyphenation, Translations}
def render_html(tree) do def render_html(tree) do
tree tree
@ -9,11 +10,6 @@ defmodule Outlook.InternalTree do
|> Html.to_html() |> Html.to_html()
end end
def render_html_preview(tree, target \\ "1") do
tree
|> Html.to_html_preview(target)
end
require Logger require Logger
def apply_modifier(tree, modifier, nids, opts \\ %{}) do def apply_modifier(tree, modifier, nids, opts \\ %{}) do
# Logger.info modifier # Logger.info modifier
@ -55,9 +51,24 @@ defmodule Outlook.InternalTree do
Translation.render_translation(tree, translation) Translation.render_translation(tree, translation)
end end
def legacy_export(tree, translation) do def render_public_content(tree, translation, language) do
Translation.render_translation(tree, translation) Translation.render_translation(tree, translation)
|> Html.render_doc()
|> Hyphenation.hyphenate(language)
end
def legacy_export(translation_id) do
translation = Translations.get_translation!(translation_id)
content= Translation.render_translation(translation.article.content, translation.content)
|> garnish(%{tunits: %{class: "ttrans", "data-ttrans-status": fn n -> Map.get(n, :status) end}}) |> garnish(%{tunits: %{class: "ttrans", "data-ttrans-status": fn n -> Map.get(n, :status) end}})
|> Html.render_doc |> Html.render_doc
|> Hyphenation.hyphenate(translation.language)
IO.puts "writing export.html to #{File.cwd!}"
File.write("export.html", content)
end
def get_tunit_ids(tree) do
InternalTree.collect_tunit_ids(tree)
# |> List.flatten()
end end
end end

View File

@ -51,30 +51,6 @@ defmodule Outlook.InternalTree.Html do
def to_html([]), do: "" def to_html([]), do: ""
def to_html_preview([ %InternalNode{type: :element} = node | rest], target_id) do
attr_string = Map.put(node.attributes, :nid, node.nid)
|> Enum.map_join(" ", fn {k,v} -> "#{k}=\"#{v}\"" end)
"<#{node.name} #{attr_string}>" <>
to_html_preview(node.content, target_id) <>
"</#{node.name}>" <>
to_html_preview(rest, target_id)
end
def to_html_preview([ %InternalNode{type: :text} = node | rest], target_id) do
~s(<span nid="#{node.nid}">#{node.content}</span>) <> to_html_preview(rest, target_id)
end
def to_html_preview([ %InternalNode{type: :comment} = node | rest], target_id) do
~s(<span nid="#{node.nid}"><!--#{node.content}--></span>) <> to_html_preview(rest, target_id)
end
def to_html_preview([ %TranslationUnit{} = tunit | rest], target_id) do
~s|<span class="tunit" nid="#{tunit.nid}" tu-status="#{tunit.status}" phx-click="select_current_tunit"
phx-value-nid="#{tunit.nid}" phx-target="#{target_id}">#{tunit.content}</span>| <> to_html_preview(rest, target_id)
end
def to_html_preview([], _target_id), do: ""
def render_doc(tree) do def render_doc(tree) do
OutlookWeb.HtmlDocComponent.render_doc(%{tree: tree}) OutlookWeb.HtmlDocComponent.render_doc(%{tree: tree})

View File

@ -58,4 +58,19 @@ defmodule Outlook.InternalTree.InternalTree do
|> Enum.into(node_atts) |> Enum.into(node_atts)
%{node | eph: Map.put(node.eph, :attributes, attributes)} %{node | eph: Map.put(node.eph, :attributes, attributes)}
end end
def collect_tunit_ids([%TranslationUnit{} = node | rest]) do
[node.nid | collect_tunit_ids(rest)]
end
def collect_tunit_ids([%{type: :element} = node | rest]) do
collect_tunit_ids(node.content) ++ collect_tunit_ids(rest)
end
def collect_tunit_ids([node | rest]) do
collect_tunit_ids(rest)
end
def collect_tunit_ids([]), do: []
end end

94
lib/outlook/public.ex Normal file
View File

@ -0,0 +1,94 @@
defmodule Outlook.Public do
@moduledoc """
This should replace Outlook.Artikel and Outlook.Autoren for
both of which embedded schemas should be created, for Artikel the schema should
implement to_param protocol (no more needed for Outlook.Translations.Translation then).
"""
alias Outlook.Translations.Translation
alias Outlook.Articles.Article
alias Outlook.Authors.Author
alias Outlook.Public.{Artikel,Autor}
import Ecto.Query, warn: false
alias Outlook.Repo
def list_artikel(language \\ "DE") do
q = from t in Translation,
join: a in Article, on: t.article_id == a.id,
join: au in Author, on: a.author_id == au.id,
select: %Artikel{
title: t.title,
date: t.date,
teaser: t.teaser,
id: t.id,
date_org: a.date,
autor_name: au.name,
},
where: t.public == true and t.language == ^language,
order_by: [desc: t.date]
Repo.all(q)
end
def get_artikel!(artikel) when is_struct(artikel), do: get_artikel!(artikel.id)
def get_artikel!(id) do
q = from t in Translation,
join: a in Article, on: t.article_id == a.id,
join: au in Author, on: a.author_id == au.id,
select: %Artikel{
title: t.title,
date: t.date,
public_content: t.public_content,
title_org: a.title,
url_org: a.url,
date_org: a.date,
autor_name: au.name,
autor_id: au.id
},
where: t.id == ^id and t.public == true
case Repo.one(q) do
nil -> {:error, "Artikel does not exist, or isn't public."}
artikel -> {:ok, artikel}
end
end
def get_artikel_by_tid(tid) do
tid
|> String.split(~r/--(?=[0-9A-Za-z])/)
|> List.last()
|> String.to_integer(36)
|> get_artikel!()
end
# for /autoren/
def list_autoren do
Repo.all(Author)
end
def get_autor!(id) do
q = from au in Author,
select: %Autor{
name: au.name,
description: au.description,
homepage_name: au.homepage_name,
homepage_url: au.homepage_url,
},
where: au.id == ^id
autor = Repo.one(q)
q2 = from a in Article,
join: t in Translation, on: t.article_id == a.id,
select: %Artikel{
title: t.title,
date: t.date,
teaser: t.teaser,
id: t.id,
date_org: a.date
},
where: a.author_id == ^id and t.public == true
artikel = Repo.all(q2)
%Autor{autor | artikel: artikel}
end
end

View File

@ -0,0 +1,43 @@
defmodule Outlook.Public.Artikel do
use Ecto.Schema
alias Outlook.Public.Artikel
embedded_schema do
field :title, :string
field :date, :utc_datetime
field :public_content, :string
field :title_org, :string
field :url_org, :string
field :date_org, :utc_datetime
field :autor_name, :string
field :autor_id, :integer
field :teaser, :string
# field :autor, Autor
end
def translate_unicode(str) do
mapping = %{"Ä" => "Ae",
"Ö" => "Oe",
"Ü" => "Ue",
"ä" => "ae",
"ö" => "oe",
"ü" => "ue",
"ß" => "ss"}
{:ok, re} = "[#{Map.keys(mapping) |> Enum.join}]" |> Regex.compile("u")
Regex.replace(re, str, fn(c) -> mapping[c] end)
end
def spit_title(title) do
title
|> translate_unicode()
|> String.replace(~r/[^\w\s-]/u, "")
|> String.replace(~r/(\s|-)+/u, "-")
end
defimpl Phoenix.Param, for: Artikel do
def to_param(%{id: id, title: title}) do
"#{Artikel.spit_title(title)}--#{Integer.to_string(id, 36) |> String.downcase()}"
end
end
end

View File

@ -0,0 +1,13 @@
defmodule Outlook.Public.Autor do
use Ecto.Schema
alias Outlook.Public.Artikel
embedded_schema do
field :name, :string
field :description, :string
field :homepage_name, :string
field :homepage_url, :string
has_many :artikel, Artikel
end
end

View File

@ -13,7 +13,7 @@ defmodule Outlook.Translations.TranslationUnitsMap do
def load(serialized_map) when is_map(serialized_map) do def load(serialized_map) when is_map(serialized_map) do
tumap = for {key, val_str} <- serialized_map do tumap = for {key, val_str} <- serialized_map do
val_map = Jason.decode!(val_str, keys: :atoms!) val_map = Jason.decode!(val_str, keys: :atoms)
val_tu = struct(TranslationUnit, %{val_map | status: String.to_atom(val_map.status)}) val_tu = struct(TranslationUnit, %{val_map | status: String.to_atom(val_map.status)})
{key, val_tu} {key, val_tu}
end end

View File

@ -48,6 +48,19 @@ defmodule Outlook.Translators do
|> Repo.update_all([inc: [our_character_count: billed_characters]]) |> Repo.update_all([inc: [our_character_count: billed_characters]])
end end
def get_uptodate_deepl_counts(user) do
deepl_counts = Deepl.get_usage_counts(get_deepl_auth_key(user))
our_character_count = deepl_account_for_user(user)
|> select([:our_character_count])
|> Repo.one()
|> Map.get(:our_character_count)
most_accurate = max(our_character_count, deepl_counts.character_count)
%{character_limit: deepl_counts.character_limit,
character_count: deepl_counts.character_count,
our_character_count: our_character_count,
percent_used: most_accurate * 100 / deepl_counts.character_limit}
end
def translate(translation, current_user) do def translate(translation, current_user) do
%{language: target_lang, %{language: target_lang,
article: %{content: article_tree, language: source_lang} article: %{content: article_tree, language: source_lang}
@ -66,6 +79,19 @@ defmodule Outlook.Translators do
Task.start_link(Deepl, :translate, args) Task.start_link(Deepl, :translate, args)
end end
def process_translation_result(result, tunit_ids, current_user) do
increase_our_character_count(current_user, result.billed_characters)
process_translation(result.translation, tunit_ids)
end
def update_deepl_counts(user, counts) do
deepl_account_for_user(user)
|> Repo.update_all([set: [
character_limit: counts.character_limit,
character_count: counts.character_count
]])
end
defp deepl_account_for_user(user) when is_struct(user), do: deepl_account_for_user(user.id) defp deepl_account_for_user(user) when is_struct(user), do: deepl_account_for_user(user.id)
defp deepl_account_for_user(user_id) do defp deepl_account_for_user(user_id) do
DeeplAccount |> where(user_id: ^user_id) DeeplAccount |> where(user_id: ^user_id)
@ -78,12 +104,7 @@ defmodule Outlook.Translators do
|> IO.iodata_to_binary() |> IO.iodata_to_binary()
end end
def process_translation_result(result, tunit_ids) do defp process_translation(translation, tunit_ids) do
# TODO: update :our_character_count
process_translation(result.translation, tunit_ids)
end
def process_translation(translation, tunit_ids) do
tunit_map = translation tunit_map = translation
|> Floki.parse_fragment! |> Floki.parse_fragment!
|> Floki.find("tunit") |> Floki.find("tunit")

View File

@ -54,11 +54,13 @@ defmodule Outlook.Translators.Deepl do
) )
response = Jason.decode!(response_raw.body, keys: :atoms) response = Jason.decode!(response_raw.body, keys: :atoms)
require Logger
case response do case response do
%{status: "done"} -> %{status: "done"} ->
response response
%{status: status} -> %{status: status} ->
steps = response.seconds_remaining * 5 Logger.debug "Deepl response: #{response |> inspect}"
steps = Map.get(response, :seconds_remaining, 1) * 5
for n <- 0..steps do for n <- 0..steps do
send(pid, {:progress, %{progress: 100 * n / steps, status: status}}) send(pid, {:progress, %{progress: 100 * n / steps, status: status}})
Process.sleep 200 Process.sleep 200

View File

@ -17,7 +17,7 @@ defmodule OutlookWeb do
those modules here. those modules here.
""" """
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) def static_paths, do: ~w(assets fonts images js favicon.ico robots.txt)
def router do def router do
quote do quote do
@ -86,9 +86,15 @@ defmodule OutlookWeb do
import Phoenix.HTML import Phoenix.HTML
# Core UI components and translation # Core UI components and translation
import OutlookWeb.CoreComponents import OutlookWeb.CoreComponents
# custom components and module
import OutlookWeb.HtmlTreeComponent import OutlookWeb.HtmlTreeComponent
import OutlookWeb.HtmlDocComponent import OutlookWeb.HtmlDocComponent
import OutlookWeb.TunitEditorComponent import OutlookWeb.TunitEditorComponent
import OutlookWeb.PublicComponents
import OutlookWeb.DarkModeComponent
import OutlookWeb.ViewHelpers
import OutlookWeb.Gettext import OutlookWeb.Gettext
# Shortcut for generating JS commands # Shortcut for generating JS commands

View File

@ -191,7 +191,7 @@ defmodule OutlookWeb.CoreComponents do
def simple_form(assigns) do def simple_form(assigns) do
~H""" ~H"""
<.form :let={f} for={@for} as={@as} {@rest}> <.form :let={f} for={@for} as={@as} {@rest}>
<div class="space-y-8 bg-white mt-10"> <div class="space-y-8 bg-transparent mt-10">
<%= render_slot(@inner_block, f) %> <%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6"> <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %> <%= render_slot(action, f) %>
@ -220,8 +220,9 @@ defmodule OutlookWeb.CoreComponents do
<button <button
type={@type} type={@type}
class={[ class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3", "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 dark:bg-gray-600 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80", "text-sm font-semibold leading-6 text-white active:text-white/80",
"dark:text-gray-300 dark:active:text-gray/80",
@class @class
]} ]}
{@rest} {@rest}
@ -281,7 +282,7 @@ defmodule OutlookWeb.CoreComponents do
assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end) assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end)
~H""" ~H"""
<label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600"> <label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600 dark:text-zinc-300">
<input type="hidden" name={@name} value="false" /> <input type="hidden" name={@name} value="false" />
<input <input
type="checkbox" type="checkbox"
@ -289,7 +290,10 @@ defmodule OutlookWeb.CoreComponents do
name={@name} name={@name}
value="true" value="true"
checked={@checked} checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900" class={[
"rounded border-zinc-300 dark:border-stone-800 dark:bg-stone-800 text-zinc-900 dark:text-zinc-200",
" focus:ring-zinc-900 dark:focus:ring-stone-700 dark:focus:bg-stone-800",
]}
{@rest} {@rest}
/> />
<%= @label %> <%= @label %>
@ -304,7 +308,7 @@ defmodule OutlookWeb.CoreComponents do
<select <select
id={@id} id={@id}
name={@name} name={@name}
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-zinc-500 focus:border-zinc-500 sm:text-sm" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-stone-900 rounded-md shadow-sm focus:outline-none focus:ring-zinc-500 focus:border-zinc-500 sm:text-sm"
multiple={@multiple} multiple={@multiple}
{@rest} {@rest}
> >
@ -328,6 +332,7 @@ defmodule OutlookWeb.CoreComponents do
"mt-2 block min-h-[6rem] w-full rounded-lg border-zinc-300 py-[7px] px-[11px]", "mt-2 block min-h-[6rem] w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
"text-zinc-900 focus:border-zinc-400 focus:outline-none focus:ring-4 focus:ring-zinc-800/5 sm:text-sm sm:leading-6", "text-zinc-900 focus:border-zinc-400 focus:outline-none focus:ring-4 focus:ring-zinc-800/5 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5", "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
"dark:border-gray-700 dark:bg-stone-900 dark:text-gray-300",
@class @class
]} ]}
{@rest} {@rest}
@ -360,7 +365,8 @@ defmodule OutlookWeb.CoreComponents do
input_border(@errors), input_border(@errors),
"mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]", "mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
"text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6", "text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5" "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
"dark:border-gray-700 dark:bg-stone-900 dark:text-gray-300",
]} ]}
{@rest} {@rest}
/> />
@ -383,7 +389,7 @@ defmodule OutlookWeb.CoreComponents do
def label(assigns) do def label(assigns) do
~H""" ~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800"> <label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800 dark:text-zinc-400">
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
</label> </label>
""" """
@ -416,10 +422,10 @@ defmodule OutlookWeb.CoreComponents do
~H""" ~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}> <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div> <div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800"> <h1 class="text-lg font-semibold leading-8 text-stone-800 dark:text-stone-300 ">
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
</h1> </h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600"> <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-stone-600">
<%= render_slot(@subtitle) %> <%= render_slot(@subtitle) %>
</p> </p>
</div> </div>
@ -458,11 +464,11 @@ defmodule OutlookWeb.CoreComponents do
<th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th> <th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
</tr> </tr>
</thead> </thead>
<tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"> <tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 dark:border-zinc-500 text-sm leading-6 text-zinc-700 dark:text-zinc-400">
<tr <tr
:for={row <- @rows} :for={row <- @rows}
id={"#{@id}-#{Phoenix.Param.to_param(row)}"} id={"#{@id}-#{Phoenix.Param.to_param(row)}"}
class="relative group hover:bg-zinc-50" class="relative group hover:bg-zinc-50 dark:hover:bg-zinc-800 "
> >
<td <td
:for={{col, i} <- Enum.with_index(@col)} :for={{col, i} <- Enum.with_index(@col)}
@ -470,11 +476,11 @@ defmodule OutlookWeb.CoreComponents do
class={["p-0", @row_click && "hover:cursor-pointer"]} class={["p-0", @row_click && "hover:cursor-pointer"]}
> >
<div :if={i == 0}> <div :if={i == 0}>
<span class="absolute h-full w-4 top-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" /> <span class="absolute h-full w-4 top-0 -left-4 group-hover:bg-zinc-50 dark:group-hover:bg-zinc-800 sm:rounded-l-xl" />
<span class="absolute h-full w-4 top-0 -right-4 group-hover:bg-zinc-50 sm:rounded-r-xl" /> <span class="absolute h-full w-4 top-0 -right-4 group-hover:bg-zinc-50 dark:group-hover:bg-zinc-800 sm:rounded-r-xl" />
</div> </div>
<div class="block py-4 pr-6"> <div class="block py-4 pr-6">
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}> <span class={["relative", i == 0 && "font-semibold text-zinc-900 dark:text-zinc-300"]}>
<%= render_slot(col, row) %> <%= render_slot(col, row) %>
</span> </span>
</div> </div>
@ -483,7 +489,7 @@ defmodule OutlookWeb.CoreComponents do
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium"> <div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span <span
:for={action <- @action} :for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700" class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700 dark:text-zinc-500 dark:hover:text-zinc-400"
> >
<%= render_slot(action, row) %> <%= render_slot(action, row) %>
</span> </span>
@ -538,7 +544,7 @@ defmodule OutlookWeb.CoreComponents do
<div class="mt-16"> <div class="mt-16">
<.link <.link
navigate={@navigate} navigate={@navigate}
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" class="text-sm font-semibold leading-6 text-stone-900 hover:text-stone-700 dark:text-stone-300 hover:dark:text-stone-200"
> >
<Heroicons.arrow_left solid class="w-3 h-3 stroke-current inline" /> <Heroicons.arrow_left solid class="w-3 h-3 stroke-current inline" />
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>

View File

@ -0,0 +1,54 @@
defmodule OutlookWeb.DarkModeComponent do
@moduledoc """
Provides components for showing and listing artikel and autoren.
"""
use Phoenix.Component
import Phoenix.HTML
# alias Phoenix.LiveView.JS
def dark_mode_widget(assigns) do
~H"""
<div id="dark-mode-widget" class="absolute flex w-full justify-between p-0" phx-hook="dark_mode_widget">
<div class="flex"></div>
<div class="flex">
<button class="p-2" type="button">
<span class="dark:hidden">
<Heroicons.sun class="w-5 h-5 stroke-stone-900 dark:stroke-stone-500" />
</span>
<span class="hidden dark:inline">
<Heroicons.moon class="w-5 h-5 stroke-stone-900 dark:stroke-stone-500" />
</span>
</button>
<ul class="hidden absolute z-50 top right-2 bg-white rounded-lg ring-1 ring-slate-900/10 shadow-lg overflow-hidden w-36 py-1 text-sm text-slate-700 font-semibold dark:bg-slate-800 dark:ring-0 dark:highlight-white/5 dark:text-slate-300 mt-8" aria-labelledby="headlessui-listbox-label-3" aria-orientation="vertical" id="headlessui-listbox-options-5" role="listbox" tabindex="0" data-headlessui-state="open">
<li class="py-1 px-2 flex items-center cursor-pointer" id="selector-option-1" role="option" tabindex="-1">
<Heroicons.sun class="w-5 h-5 mr-2 stroke-slate-400 dark:stroke-slate-500" />
Light
</li>
<li class="py-1 px-2 flex items-center cursor-pointer" id="selector-option-2" role="option" tabindex="-1">
<Heroicons.moon class="w-5 h-5 mr-2 stroke-slate-400 dark:stroke-slate-500" />
Dark
</li>
<li class="py-1 px-2 flex items-center cursor-pointer text-slate-700 dark:text-slate-300" id="selector-option-3" role="option" tabindex="-1" aria-selected="true" data-headlessui-state="selected">
<Heroicons.computer_desktop class="w-5 h-5 mr-2 stroke-slate-400 dark:stroke-slate-500" />
System
</li>
</ul>
</div>
</div>
"""
end
def breakpoint_indicator(assigns) do
~H"""
<div class="absolute p-1 bg-white text-black dark:bg-black dark:text-white">
<span class="sm:hidden">xs</span>
<span class="hidden sm:inline md:hidden">sm</span>
<span class="hidden md:inline lg:hidden">md</span>
<span class="hidden lg:inline xl:hidden">lg</span>
<span class="hidden xl:inline 2xl:hidden">xl</span>
<span class="hidden 2xl:inline">2xl</span>
</div>
"""
end
end

View File

@ -8,17 +8,13 @@ defmodule OutlookWeb.HtmlDocComponent do
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
attr :tree, :list, required: true attr :tree, :list, required: true
attr :tunit_tag, :atom, default: :span
def render_doc(%{tunit_tag: _} = assigns) do def render_doc(assigns) do
~H""" ~H"""
<.dnode :for={node <- @tree} node={node} tunit_tag={@tunit_tag} /> <.dnode :for={node <- @tree} node={node} tunit_tag={@tunit_tag} />
""" """
end end
def render_doc(assigns) do
assigns
|> Map.put(:tunit_tag, "span")
|> render_doc()
end
def dnode(%{node: %{status: _}} = assigns) do def dnode(%{node: %{status: _}} = assigns) do
~H""" ~H"""

View File

@ -6,52 +6,40 @@ defmodule OutlookWeb.HtmlTreeComponent do
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
attr :tree_items, :list, required: true attr :tree, :list, required: true
def treeview(assigns) do def render_tree(assigns) do
~H""" ~H"""
<div class="font-mono whitespace-nowrap"> <.tnode :for={node <- @tree} node={node} />
<%= for tree_item <- @tree_items do %>
<%= case tree_item do %>
<% %{node: %{type: :element}} = item -> %>
<.tree_element node={item.node} level={item.level}></.tree_element>
<% %{node: %{type: :text}} = item -> %>
<.tree_text node={item.node} level={item.level}></.tree_text>
<% %{node: %{type: :comment}} = item -> %>
<.tree_comment node={item.node} level={item.level}></.tree_comment>
<% end %>
<% end %>
</div>
<.link phx-click={JS.push("apply_modifier", value: %{modifier: :unwrap})}>
<.button title="unwraps selected elements">Unwrap</.button>
</.link>
<.link phx-click={JS.push("partition_text", value: %{modifier: :unwrap})}>
<.button title="splits text into sentences">Partition</.button>
</.link>
""" """
end end
def tree_element(assigns) do def attributes(assigns) do
~H"&nbsp; <%= @name %>=&quot;<%= @value %>&quot;"
end
def tnode(%{node: %{status: _}} = assigns), do: ~H"<%= String.slice(@node.content, 0..20) %><%= if String.length(@node.content) > 20 do %>...<% end %><br>"
def tnode(assigns) when assigns.node.type == :element do
~H""" ~H"""
<div nid={@node.nid} phx-click={JS.push("select", value: %{nid: @node.nid})}> &lt;<%= @node.name %><.attributes :for={{k,v} <- @node.attributes} name={k} value={v}
<%= "#{String.duplicate("  ", @level)}<#{@node.name}>" %> />&gt;<br>
</div> <div class="ml-8">
<.tnode :for={child_node <- @node.content} node={child_node} />
</div>
""" """
end end
def tree_text(assigns) do def tnode(assigns) when assigns.node.type == :text do
~H""" ~H"""
<div nid={@node.nid} phx-click={JS.push("select", value: %{nid: @node.nid})}> "<%= String.slice(@node.content, 0..35) %><%= if String.length(@node.content) > 35 do %>..."<% end %><br>
<%= "#{String.duplicate("  ", @level)}\"#{String.slice(@node.content, 0, 15)}...\"\n" %>
</div>
""" """
end end
def tree_comment(assigns) do def tnode(assigns) when assigns.node.type == :comment do
~H""" ~H"""
<div nid={@node.nid} phx-click={JS.push("select", value: %{nid: @node.nid})} title={@node.content}> &lt;!--<%= String.slice(@node.content, 0..35) %>
<%= "#{String.duplicate("  ", @level)}<!-- #{String.slice(@node.content, 0, 15)}...-->\n" %> <%= if String.length(@node.content) > 35 do %>"..."<% end %>--&gt;
</div>
""" """
end end
end end

View File

@ -34,6 +34,7 @@
</a> </a>
</div> </div>
</div> </div>
<.dark_mode_widget />
</header> </header>
<main class="px-4 py-20 sm:px-6 lg:px-8"> <main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl">

View File

@ -7,11 +7,12 @@
<.live_title suffix=" · Ausblick"> <.live_title suffix=" · Ausblick">
<%= assigns[:page_title] %> <%= assigns[:page_title] %>
</.live_title> </.live_title>
<script type="text/javascript" src={~p"/js/dark-mode.js"}></script>
<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>
</head> </head>
<body class="bg-white antialiased max-h-screen"> <body class="bg-white text-stone-900 dark:bg-stone-900 dark:text-stone-100 antialiased max-h-screen">
<%= @inner_content %> <%= @inner_content %>
</body> </body>
</html> </html>

View File

@ -0,0 +1,17 @@
<header class="">
<.breakpoint_indicator :if={Mix.env == :dev} />
<a href="/">
<img class="w-full dark:hidden" src="/images/elbefoto-lg.jpg"
src-set="elbefoto-xxl.jpg 4496w, /images/elbefoto-lg.jpg 2248w, /images/elbefoto-md.jpg 1199w, /images/elbefoto-sm.jpg 991w, /images/elbefoto-xs.jpg 767w">
<img class="w-full hidden dark:inline" src="/images/nadjas-nachtfoto-xxl.jpg"
src-set="nadjas-nachtfoto-xxl.jpg 1280w, /images/nadjas-nachtfoto-md.jpg 1199w, /images/nadjas-nachtfoto-sm.jpg 991w, /images/nadjas-nachtfoto-xs.jpg 767w">
</a>
<.dark_mode_widget />
</header>
<main class="px-2 py-4 sm:px-6 lg:px-8">
<div class="mx-auto max-w-xl">
<.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} />
<%= @inner_content %>
</div>
</main>

View File

@ -7,11 +7,12 @@
<.live_title suffix=" · Phoenix Framework"> <.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "Outlook" %> <%= assigns[:page_title] || "Outlook" %>
</.live_title> </.live_title>
<script type="text/javascript" src={~p"/js/dark-mode.js"}></script>
<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>
</head> </head>
<body class="bg-white antialiased max-h-screen"> <body class="bg-white text-stone-900 dark:bg-stone-900 dark:text-stone-100 antialiased max-h-screen">
<ul> <ul>
<%= if @current_user do %> <%= if @current_user do %>
<li> <li>
@ -24,9 +25,6 @@
<.link href={~p"/users/log_out"} method="delete">Log out</.link> <.link href={~p"/users/log_out"} method="delete">Log out</.link>
</li> </li>
<% else %> <% else %>
<li>
<.link href={~p"/users/register"}>Register</.link>
</li>
<li> <li>
<.link href={~p"/users/log_in"}>Log in</.link> <.link href={~p"/users/log_in"}>Log in</.link>
</li> </li>

View File

@ -0,0 +1,40 @@
defmodule OutlookWeb.PublicComponents do
@moduledoc """
Provides components for showing and listing artikel and autoren.
"""
use Phoenix.Component
import OutlookWeb.ViewHelpers
use OutlookWeb, :verified_routes
alias Phoenix.LiveView.JS
attr :autor, :any, required: true
def autor(assigns) do
~H"""
<a href={~p"/autoren/#{@autor}"}>
<div class="p-4 my-2 border rounded-lg border-stone-400 text-stone-800 dark:text-stone-300 ">
<div class="font-bold"><%= @autor.name %></div>
<div class=""><%= @autor.description |> tidy_raw %></div>
</div>
</a>
"""
end
attr :artikel, :any, required: true
attr :show_autor, :boolean, default: true
def artikel(assigns) do
~H"""
<.link navigate={~p"/artikel/#{@artikel}"}>
<div class="my-2 px-2 rounded border-2 border-solid border-gray-300 dark:border-stone-800">
<h4 class="font-bold text-stone-800 dark:text-stone-300 py-2"><%= @artikel.title %></h4>
<div :if={@show_autor}><small><%= @artikel.autor_name %></small></div>
<div><small><%= @artikel.date |> Calendar.strftime("%d.%m.%Y") %></small></div>
<div><%= @artikel.teaser |> tidy_raw %></div>
</div>
</.link>
"""
end
end

View File

@ -14,7 +14,7 @@ defmodule OutlookWeb.TunitEditorComponent do
<%= @current_tunit.content |> raw %> <%= @current_tunit.content |> raw %>
</div> --%> </div> --%>
<form phx-change="update_current_tunit" phx-target={@target}> <form phx-change="update_current_tunit" phx-target={@target}>
<textarea name="content" class="h-48 rounded border-slate-500 resize-none w-full" <textarea id="tunit-editor-content" name="content" class="h-48 rounded border-slate-500 resize-none bg-transparent w-full"
disabled={!@current_tunit.status}><%= @current_tunit.content %></textarea> disabled={!@current_tunit.status}><%= @current_tunit.content %></textarea>
</form> </form>
<.status_selector target={@target} disabled={!@current_tunit.status} tunit={@current_tunit} /> <.status_selector target={@target} disabled={!@current_tunit.status} tunit={@current_tunit} />

View File

@ -1,15 +1,21 @@
defmodule OutlookWeb.ArtikelController do defmodule OutlookWeb.ArtikelController do
use OutlookWeb, :controller use OutlookWeb, :controller
alias Outlook.Artikel alias Outlook.Public
def index(conn, _params) do def index(conn, _params) do
artikel = Artikel.list_artikel() artikel = Public.list_artikel()
render(conn, :index, artikel: artikel, page_title: "Artikel") render(conn, :index, artikel: artikel, page_title: "Artikel")
end end
def show(conn, %{"id" => id}) do def show(conn, %{"tid" => tid} = params) do
artikel = Artikel.get_artikel!(id) case Public.get_artikel_by_tid(tid) do
render(conn, :show, artikel: artikel, page_title: artikel.title) {:ok, artikel} -> render(conn, :show, artikel: artikel, page_title: artikel.title)
{:error, message} -> conn
|> put_status(404)
|> put_view(OutlookWeb.ErrorHTML)
|> render("404.html")
|> halt()
end
end end
end end

View File

@ -1,19 +1,2 @@
<.header>
Listing Artikel
<:actions>
</:actions>
</.header>
<.table id="artikel" rows={@artikel} row_click={&JS.navigate(~p"/artikel/#{&1}")}> <.artikel :for={artikel <- @artikel} artikel={artikel} />
<:col :let={artikel} label="Title"><%= artikel.title %></:col>
<:col :let={artikel} label="Teaser"><%= artikel.teaser %></:col>
<%!-- <:col :let={artikel} label="Translator"><%= artikel.translator %></:col> --%>
<:col :let={artikel} label="Unauthorized"><%= artikel.unauthorized %></:col>
<:col :let={artikel} label="Public content"><%= artikel.public_content %></:col>
<:col :let={artikel} label="Date"><%= Calendar.strftime(artikel.date, "%d.%m.%Y") %></:col>
<:action :let={artikel}>
<div class="sr-only">
<.link navigate={~p"/artikel/#{artikel}"}>Show</.link>
</div>
</:action>
</.table>

View File

@ -1,19 +1,17 @@
<.header> <header class="mb-6">
<%= @artikel.title %> <h1 class="text-lg font-semibold leading-tight text-stone-800 dark:text-stone-200"><%= @artikel.title %></h1>
<:subtitle><%= @artikel.article.author.name %></:subtitle> <p class="my-2"><.link href={~p"/autoren/#{@artikel.autor_id}"}><%= @artikel.autor_name %></.link>
<:actions> &nbsp;&nbsp;&nbsp; — &nbsp;&nbsp;&nbsp;<%= Calendar.strftime(@artikel.date_org, "%d.%m.%Y") %></p>
<.link href={@artikel.article.url} > <div>Original Artikel:
<%= @artikel.article.title %> <.link class="hover:text-sky-700" href={@artikel.url_org} >
</.link> <%= Calendar.strftime(@artikel.article.date, "%d.%m.%Y") %> <%= @artikel.title_org %>
</:actions> </.link><br>
</.header> </div>
<div class="my-2">
Übersetzung: <%= Calendar.strftime(@artikel.date, "%d.%m.%Y") %>
</div>
</header>
<.list> <div class="article w-full mx-auto max-w-xs"><%= @artikel.public_content |> raw %></div>
<:item title="Title"><%= @artikel.title %></:item>
<%!-- <:item title="Translator"><%= @artikel.translator %></:item> --%>
<:item title="Unauthorized"><%= @artikel.unauthorized %></:item>
<:item title="Date"><%= Calendar.strftime(@artikel.date, "%d.%m.%Y") %></:item>
</.list>
<div class="article"><%= @artikel.public_content |> raw %></div>
<.back navigate={~p"/autoren/#{@artikel.article.author}"}>Back to Autor</.back> <.back navigate={~p"/autoren/#{@artikel.autor_id}"}>Back to Autor</.back>

View File

@ -1,15 +1,15 @@
defmodule OutlookWeb.AutorController do defmodule OutlookWeb.AutorController do
use OutlookWeb, :controller use OutlookWeb, :controller
alias Outlook.Autoren alias Outlook.Public
def index(conn, _params) do def index(conn, _params) do
autoren = Autoren.list_autoren() autoren = Public.list_autoren()
render(conn, :index, autoren: autoren, page_title: "Autoren") render(conn, :index, autoren: autoren, page_title: "Autoren")
end end
def show(conn, %{"id" => id}) do def show(conn, %{"id" => id}) do
autor = Autoren.get_autor!(id) autor = Public.get_autor!(id)
# artikel = Autoren.list_artikel(autor) # artikel = Autoren.list_artikel(autor)
render(conn, :show, autor: autor, page_title: autor.name) render(conn, :show, autor: autor, page_title: autor.name)
end end

View File

@ -1,20 +1,5 @@
<.header> <.header>
Listing Autoren Autoren
<:actions>
<.link href={~p"/autoren/new"}>
<.button>New Autor</.button>
</.link>
</:actions>
</.header> </.header>
<.table id="autoren" rows={@autoren} row_click={&JS.navigate(~p"/autoren/#{&1}")}> <.autor :for={autor <- @autoren} autor={autor} />
<:col :let={autor} label="Name"><%= autor.name %></:col>
<:col :let={autor} label="Description"><%= autor.description %></:col>
<:col :let={autor} label="Homepage name"><%= autor.homepage_name %></:col>
<:col :let={autor} label="Homepage url"><%= autor.homepage_url %></:col>
<:action :let={autor}>
<div class="sr-only">
<.link navigate={~p"/autoren/#{autor}"}>Show</.link>
</div>
</:action>
</.table>

View File

@ -1,16 +1,9 @@
<.header> <.header>
<%= @autor.name %> <%= @autor.name %>
<:subtitle><div class="text-lg mb-2"><%= @autor.description %></div></:subtitle> <:subtitle><div class="text-lg mb-2"><%= @autor.description |> tidy_raw %></div></:subtitle>
<:subtitle><.link href={@autor.homepage_url}><%= @autor.homepage_name %></.link></:subtitle> <:subtitle><.link href={@autor.homepage_url}><%= @autor.homepage_name %></.link></:subtitle>
</.header> </.header>
<.artikel :for={artikel <- @autor.artikel} artikel={artikel} show_autor={false} />
<%= for article <- @autor.articles do %>
<div :for={translation <- article.translations} class="my-2 px-2 rounded border-2 border-solid border-gray-300">
<.link navigate={~p"/artikel/#{translation}"}><h4 class="font-bold py-2"><%= translation.title %></h4></.link>
<div><%= translation.teaser %></div>
</div>
<% end %>
<.back navigate={~p"/autoren"}>Back to autoren</.back> <.back navigate={~p"/autoren"}>Back to autoren</.back>

View File

@ -34,7 +34,7 @@ defmodule OutlookWeb.ArticleLive.NewComponents do
<%= InternalTree.render_html_preview(@raw_internal_tree) |> raw %> <%= InternalTree.render_html_preview(@raw_internal_tree) |> raw %>
</div> </div>
<div id="html-tree"> <div id="html-tree">
<.treeview tree_items={HtmlPreparations.get_tree_items(@raw_internal_tree)} ></.treeview> <.render_tree tree={@raw_internal_tree} ></.render_tree>
</div> </div>
</div> </div>
<.button phx-click="approve_raw_internaltree">Continue</.button> <.button phx-click="approve_raw_internaltree">Continue</.button>

View File

@ -1,8 +1,7 @@
defmodule OutlookWeb.ArticleLive.Show do defmodule OutlookWeb.ArticleLive.Show do
use OutlookWeb, :live_view use OutlookWeb, :live_view
alias Outlook.Articles alias Outlook.{Articles,InternalTree,Translations}
alias Outlook.InternalTree
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -11,12 +10,24 @@ defmodule OutlookWeb.ArticleLive.Show do
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(%{"id" => id}, _, socket) do
{:noreply, socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> get_and_assign_article(id)}
end
@impl true
def handle_event("delete_translation", %{"id" => id}, socket) do
translation = Translations.get_translation!(id)
{:ok, _} = Translations.delete_translation(translation)
{:noreply, socket |> get_and_assign_article(socket.assigns.article.id)}
end
defp get_and_assign_article(socket, id) do
article = Articles.get_article_with_translations!(id) article = Articles.get_article_with_translations!(id)
{:noreply, socket
socket |> assign(:article_content, InternalTree.garnish(article.content, %{tunits: %{class: "tunit"}}))
|> assign(:page_title, page_title(socket.assigns.live_action)) |> assign(:article, article)
|> assign(:article_content, InternalTree.garnish(article.content, %{tunits: %{class: "tunit"}}))
|> assign(:article, article)}
end end
defp page_title(:show), do: "Show Article" defp page_title(:show), do: "Show Article"

View File

@ -19,7 +19,7 @@
<.table id="translations" rows={@article.translations} row_click={&JS.navigate(~p"/translations/#{&1}")}> <.table id="translations" rows={@article.translations} row_click={&JS.navigate(~p"/translations/#{&1}")}>
<:col :let={translation} label="Language"><%= translation.language %></:col> <:col :let={translation} label="Language"><%= translation.language %></:col>
<:col :let={translation} label="Title"><%= translation.title %></:col> <:col :let={translation} label="Title"><%= translation.title %></:col>
<:col :let={translation} label="Teaser"><%= translation.teaser %></:col> <:col :let={translation} label="Teaser"><%= translation.teaser |> tidy_raw %></:col>
<:col :let={translation} label="Date"><%= translation.date %></:col> <:col :let={translation} label="Date"><%= translation.date %></:col>
<:col :let={translation} label="Public"><%= translation.public %></:col> <:col :let={translation} label="Public"><%= translation.public %></:col>
<:action :let={translation}> <:action :let={translation}>
@ -28,11 +28,11 @@
</div> </div>
<.link navigate={~p"/translations/#{translation}/edit"}>Edit</.link> <.link navigate={~p"/translations/#{translation}/edit"}>Edit</.link>
</:action> </:action>
<%!-- <:action :let={translation}> <:action :let={translation}>
<.link phx-click={JS.push("delete", value: %{id: translation.id})} data-confirm="Are you sure?"> <.link phx-click={JS.push("delete_translation", value: %{id: translation.id})} data-confirm="Are you sure?">
Delete Delete
</.link> </.link>
</:action> --%> </:action>
</.table> </.table>
<div class="article"> <div class="article">
@ -40,10 +40,11 @@
<a href="#" class="hide-link" phx-click={JS.remove_class("show-boundary", to: ".article")}>hide boundaries</a> <a href="#" class="hide-link" phx-click={JS.remove_class("show-boundary", to: ".article")}>hide boundaries</a>
<.render_doc tree={@article_content} /> <.render_doc tree={@article_content} />
</div> </div>
<div class="h-10" />
<.link navigate={~p"/translations/new?article_id=#{@article.id}"}>New Translation</.link> <.link class="text-sm font-semibold" navigate={~p"/translations/new?article_id=#{@article.id}"}>New Translation</.link>
<.back navigate={~p"/articles"}>Back to articles</.back> <.back navigate={~p"/authors/#{@article.author}"}>Back to author</.back>
<.modal :if={@live_action == :edit} id="article-modal" show on_cancel={JS.patch(~p"/articles/#{@article}")}> <.modal :if={@live_action == :edit} id="article-modal" show on_cancel={JS.patch(~p"/articles/#{@article}")}>
<.live_component <.live_component

View File

@ -9,7 +9,7 @@
<.table id="authors" rows={@authors} row_click={&JS.navigate(~p"/authors/#{&1}")}> <.table id="authors" rows={@authors} row_click={&JS.navigate(~p"/authors/#{&1}")}>
<:col :let={author} label="Name"><%= author.name %></:col> <:col :let={author} label="Name"><%= author.name %></:col>
<:col :let={author} label="Description"><%= author.description %></:col> <:col :let={author} label="Description"><%= author.description |> tidy_raw %></:col>
<:col :let={author} label="Homepage name"><%= author.homepage_name %></:col> <:col :let={author} label="Homepage name"><%= author.homepage_name %></:col>
<:col :let={author} label="Homepage url"><%= author.homepage_url %></:col> <:col :let={author} label="Homepage url"><%= author.homepage_url %></:col>
<:action :let={author}> <:action :let={author}>

View File

@ -1,7 +1,7 @@
defmodule OutlookWeb.AuthorLive.Show do defmodule OutlookWeb.AuthorLive.Show do
use OutlookWeb, :live_view use OutlookWeb, :live_view
alias Outlook.Authors alias Outlook.{Authors,Articles}
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -16,6 +16,15 @@ defmodule OutlookWeb.AuthorLive.Show do
|> assign(:author, Authors.get_author_with_articles!(id))} |> assign(:author, Authors.get_author_with_articles!(id))}
end end
@impl true
def handle_event("delete_article", %{"id" => id}, socket) do
article = Articles.get_article!(id)
{:ok, _} = Articles.delete_article(article)
{:noreply, socket
|> assign(:author, Authors.get_author_with_articles!(socket.assigns.author.id))}
end
defp page_title(:show), do: "Show Author" defp page_title(:show), do: "Show Author"
defp page_title(:edit), do: "Edit Author" defp page_title(:edit), do: "Edit Author"
end end

View File

@ -31,7 +31,7 @@
<.link patch={~p"/articles/#{article}/edit"}>Edit</.link> <.link patch={~p"/articles/#{article}/edit"}>Edit</.link>
</:action> </:action>
<:action :let={article}> <:action :let={article}>
<.link phx-click={JS.push("delete", value: %{id: article.id})} data-confirm="Are you sure?"> <.link phx-click={JS.push("delete_article", value: %{id: article.id})} data-confirm="Are you sure?">
Delete Delete
</.link> </.link>
</:action> </:action>

View File

@ -8,7 +8,7 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="flex gap-8 max-h-fit"> <div class="flex gap-8 max-h-fit">
<div class="basis-1/2 overflow-auto"> <div class="basis-1/2 overflow-auto" id="translation-form-container" target="@myself" phx-hook="translation_form">
<.header> <.header>
<%= @title %> <%= @title %>
<:subtitle>Use this form to manage translation records in your database.</:subtitle> <:subtitle>Use this form to manage translation records in your database.</:subtitle>
@ -36,11 +36,11 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
<input type="hidden" id="publish" name="publish" value="false" /> <input type="hidden" id="publish" name="publish" value="false" />
<:actions> <:actions>
<.button phx-click={JS.set_attribute({"value", "false"}, to: "#continue_edit") |> JS.set_attribute({"value", "false"}, to: "#publish")} <.button phx-click={JS.set_attribute({"value", "false"}, to: "#continue_edit") |> JS.set_attribute({"value", "false"}, to: "#publish")}
phx-disable-with="Saving...">Save Translation</.button> id="save-button" phx-disable-with="Saving...">Save Translation</.button>
<.button phx-click={JS.set_attribute({"value", "false"}, to: "#continue_edit") |> JS.set_attribute({"value", "true"}, to: "#publish")} <.button phx-click={JS.set_attribute({"value", "false"}, to: "#continue_edit") |> JS.set_attribute({"value", "true"}, to: "#publish")}
phx-disable-with="Saving...">Save and Publish</.button> id="save-publish-button" phx-disable-with="Saving...">Save and Publish</.button>
<.button phx-click={JS.set_attribute({"value", "true"}, to: "#continue_edit") |> JS.set_attribute({"value", "false"}, to: "#publish")} <.button phx-click={JS.set_attribute({"value", "true"}, to: "#continue_edit") |> JS.set_attribute({"value", "false"}, to: "#publish")}
phx-disable-with="Saving...">Save and Edit</.button> id="save-edit-button" phx-disable-with="Saving...">Save and Edit</.button>
</:actions> </:actions>
</.simple_form> </.simple_form>
<.tunit_editor current_tunit={@current_tunit} target={@myself} /> <.tunit_editor current_tunit={@current_tunit} target={@myself} />
@ -63,6 +63,7 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
socket socket
|> assign(assigns) |> assign(assigns)
|> assign(:current_tunit, %TranslationUnit{status: nil}) |> assign(:current_tunit, %TranslationUnit{status: nil})
|> assign(:tunit_ids, InternalTree.get_tunit_ids(translation.article.content))
|> assign(:changeset, changeset) |> assign(:changeset, changeset)
|> assign_article_tree(translation) |> assign_article_tree(translation)
|> assign(:deepl_progress, nil)} |> assign(:deepl_progress, nil)}
@ -118,11 +119,33 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
{:noreply, socket |> assign(:current_tunit, tunit)} {:noreply, socket |> assign(:current_tunit, tunit)}
end end
def handle_event("select_current_tunit", %{"nid" => nid}, socket) do def handle_event("select_tunit_by_nid", %{"nid" => nid}, socket) do
{:noreply, {:noreply, change_tunit(socket, nid)}
socket end
|> update_translation_with_current_tunit(socket.assigns.current_tunit.status)
|> assign(:current_tunit, socket.assigns.translation_content[nid])} def handle_event("select_next_tunit", _, socket) do
{:noreply, select_next_tunit(socket, &Kernel.+/2)}
end
def handle_event("select_previous_tunit", _, socket) do
{:noreply, select_next_tunit(socket, &Kernel.-/2)}
end
defp select_next_tunit(socket, direction) do
tunit_ids = socket.assigns.tunit_ids
current_tunit_nid = socket.assigns.current_tunit.status && socket.assigns.current_tunit.nid || List.last(tunit_ids)
index_current = Enum.find_index(tunit_ids, fn nid -> nid == current_tunit_nid end)
index_next = direction.(index_current, 1) |> Integer.mod(length(tunit_ids))
next_nid = Enum.at(tunit_ids, index_next)
change_tunit(socket, next_nid)
end
defp change_tunit(socket, nid) do
fun = fn n -> n.nid == nid && "yes" || "no" end
socket
|> assign(:article_tree, InternalTree.garnish(socket.assigns.article_tree, %{tunits: %{current: fun}}))
|> update_translation_with_current_tunit(socket.assigns.current_tunit.status)
|> assign(:current_tunit, socket.assigns.translation_content[nid])
end end
@doc "updating on browser events" @doc "updating on browser events"
@ -146,8 +169,9 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
:article_tree, :article_tree,
InternalTree.add_phx_click_event(translation.article.content, InternalTree.add_phx_click_event(translation.article.content,
nodes: :tunits, nodes: :tunits,
click: "select_current_tunit", click: "select_tunit_by_nid",
target: socket.assigns.myself)) target: socket.assigns.myself)
|> InternalTree.garnish(%{tunits: %{class: "tunit"}}))
end end
defp update_translation_with_current_tunit(socket, nil), do: socket defp update_translation_with_current_tunit(socket, nil), do: socket
@ -164,7 +188,7 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "Translation updated successfully") |> put_flash(:info, "Translation updated successfully")
|> continue_edit(params)} |> continue_edit(:edit, params)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
@ -173,30 +197,37 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
defp save_translation(socket, :new, %{"translation" => translation_params} = params) do defp save_translation(socket, :new, %{"translation" => translation_params} = params) do
case Translations.create_translation(translation_params) do case Translations.create_translation(translation_params) do
{:ok, _translation} -> {:ok, translation} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "Translation created successfully") |> put_flash(:info, "Translation created successfully")
|> continue_edit(params)} |> continue_edit(:new, Map.put(params,"id", translation.id))}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)} {:noreply, assign(socket, changeset: changeset)}
end end
end end
defp continue_edit(socket, %{"continue_edit" => "true"}) do defp continue_edit(socket, :edit, %{"continue_edit" => "true"}) do
socket socket
|> assign(:action, :edit) |> assign(:translation, Translations.get_translation!(socket.assigns.translation.id))
end end
defp continue_edit(socket, %{"continue_edit" => "false"}) do defp continue_edit(socket, :new, %{"continue_edit" => "true"} = params) do
socket |> push_patch(to: ~p(/translations/#{params["id"]}/edit))
end
defp continue_edit(socket, _, %{"continue_edit" => "false"}) do
socket |> push_navigate(to: socket.assigns.navigate) socket |> push_navigate(to: socket.assigns.navigate)
end end
defp publish(translation_params, %{"publish" => "true"}, socket) do defp publish(translation_params, %{"publish" => "true"}, socket) do
translation_params translation_params
|> Map.put("public_content", |> Map.put("public_content",
InternalTree.render_translation(socket.assigns.translation.article.content, translation_params["content"]) InternalTree.render_public_content(
|> Html.render_doc()) socket.assigns.translation.article.content,
socket.assigns.translation_content,
socket.assigns.translation.language
)
)
end end
defp publish(translation_params, %{"publish" => "false"}, _) do defp publish(translation_params, %{"publish" => "false"}, _) do
translation_params translation_params

View File

@ -2,10 +2,10 @@
Listing Translations Listing Translations
</.header> </.header>
<.table id="translations" rows={@translations} row_click={&JS.navigate(~p"/translations/#{&1}")}> <.table id="translations" rows={@translations} row_click={&JS.navigate(~p(/translations/#{&1}))}>
<:col :let={translation} label="Language"><%= translation.language %></:col> <:col :let={translation} label="Language"><%= translation.language %></:col>
<:col :let={translation} label="Title"><%= translation.title %></:col> <:col :let={translation} label="Title"><%= translation.title %></:col>
<:col :let={translation} label="Teaser"><%= translation.teaser %></:col> <:col :let={translation} label="Teaser"><%= translation.teaser |> tidy_raw %></:col>
<%!-- <:col :let={translation} label="Content"><%= translation.content %></:col> --%> <%!-- <:col :let={translation} label="Content"><%= translation.content %></:col> --%>
<:col :let={translation} label="Date"><%= translation.date %></:col> <:col :let={translation} label="Date"><%= translation.date %></:col>
<:col :let={translation} label="Public"><%= translation.public %></:col> <:col :let={translation} label="Public"><%= translation.public %></:col>
@ -22,19 +22,3 @@
</.link> </.link>
</:action> </:action>
</.table> </.table>
<.modal
:if={@live_action in [:new, :edit]}
id="translation-modal"
show
on_cancel={JS.navigate(~p"/translations")}
>
<.live_component
module={OutlookWeb.TranslationLive.FormComponent}
id={@translation.id || :new}
title={@page_title}
action={@live_action}
translation={@translation}
navigate={~p"/translations"}
/>
</.modal>

View File

@ -24,7 +24,7 @@ defmodule OutlookWeb.TranslationLive.NewEdit do
end end
@impl true @impl true
def mount(%{"article_id" => article_id} = _params, _session, socket) when socket.assigns.live_action == :new do def handle_params(%{"article_id" => article_id} = _params, _session, socket) when socket.assigns.live_action == :new do
socket = socket socket = socket
|> assign_new(:translation, fn -> |> assign_new(:translation, fn ->
%Translation{ %Translation{
@ -33,15 +33,15 @@ defmodule OutlookWeb.TranslationLive.NewEdit do
} }
end) end)
|> common_assigns() |> common_assigns()
{:ok, assign_new(socket, :translation_content, fn -> {:noreply, assign_new(socket, :translation_content, fn ->
Basic.internal_tree_to_tunit_map(socket.assigns.translation.article.content) end)} Basic.internal_tree_to_tunit_map(socket.assigns.translation.article.content) end)}
end end
def mount(%{"id" => id} = _params, _session, socket) when socket.assigns.live_action == :edit do def handle_params(%{"id" => id} = _params, _session, socket) when socket.assigns.live_action == :edit do
socket = socket socket = socket
|> assign_new(:translation, fn -> Translations.get_translation!(id) end) |> assign(:translation, Translations.get_translation!(id))
|> common_assigns() |> common_assigns()
{:ok, assign_new(socket, :translation_content, fn -> socket.assigns.translation.content end)} {:noreply, assign(socket, :translation_content, socket.assigns.translation.content)}
end end
defp get_article(article_id) do defp get_article(article_id) do

View File

@ -10,10 +10,18 @@ defmodule OutlookWeb.TranslationLive.Show do
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(%{"id" => id}, _, socket) do
translation = Translations.get_translation!(id)
{:noreply, {:noreply,
socket socket
|> assign(:page_title, page_title(socket.assigns.live_action)) |> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:translation, Translations.get_translation!(id))} |> assign(:translation, translation)
|> assign(:translation_tree,
InternalTree.render_translation(
translation.article.content, translation.content
) |> InternalTree.garnish(
%{tunits: %{status: fn n -> n.status end, class: :tunit}}
)
)}
end end
defp page_title(:show), do: "Show Translation" defp page_title(:show), do: "Show Translation"

View File

@ -18,8 +18,8 @@
<:item title="Unauthorized"><%= @translation.unauthorized %></:item> <:item title="Unauthorized"><%= @translation.unauthorized %></:item>
</.list> </.list>
<div class="article"> <div class="article show_status">
<.render_doc tree={InternalTree.render_translation(@translation.article.content, @translation.content)} /> <.render_doc tree={@translation_tree} />
</div> </div>
<.back navigate={~p"/translations"}>Back to translations</.back> <.back navigate={~p"/articles/#{@translation.article}"}>Back to <article></article></.back>

View File

@ -7,11 +7,13 @@ defmodule OutlookWeb.UserLoginLive do
<.header class="text-center"> <.header class="text-center">
Sign in to account Sign in to account
<:subtitle> <:subtitle>
<%= unless System.get_env("DISABLE_REGISTRATION") do %>
Don't have an account? Don't have an account?
<.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline"> <.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline">
Sign up Sign up
</.link> </.link>
for an account now. for an account now.
<% end %>
</:subtitle> </:subtitle>
</.header> </.header>

View File

@ -0,0 +1,20 @@
defmodule Outlook.PreventRegistration do
import Plug.Conn
import Phoenix.Controller
def prevent_registration(conn, _) do
if System.get_env("DISABLE_REGISTRATION") && is_registration_path(conn) do
conn
|> put_flash(:error, "User Registration is disabled.")
|> redirect(to: "/users/log_in")
|> halt()
else
conn
end
end
defp is_registration_path(conn) do
"/users/register" == current_path(conn, %{})
end
end

View File

@ -3,6 +3,8 @@ defmodule OutlookWeb.Router do
import OutlookWeb.UserAuth import OutlookWeb.UserAuth
import Outlook.PreventRegistration
pipeline :browser do pipeline :browser do
plug :accepts, ["html"] plug :accepts, ["html"]
plug :fetch_session plug :fetch_session
@ -15,6 +17,11 @@ defmodule OutlookWeb.Router do
pipeline :public_root_layout do pipeline :public_root_layout do
plug :put_root_layout, "proot.html" plug :put_root_layout, "proot.html"
plug :put_layout, {OutlookWeb.Layouts, :public}
end
pipeline :check_registration do
plug :prevent_registration
end end
pipeline :api do pipeline :api do
@ -22,9 +29,12 @@ defmodule OutlookWeb.Router do
end end
scope "/", OutlookWeb do scope "/", OutlookWeb do
pipe_through :browser pipe_through [:browser, :public_root_layout]
get "/", PageController, :home get "/", ArtikelController, :index
resources "/autoren", AutorController, only: [:index, :show]
resources "/artikel", ArtikelController, only: [:index, :show], param: "tid"
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
@ -52,7 +62,7 @@ defmodule OutlookWeb.Router do
## Authentication routes ## Authentication routes
scope "/", OutlookWeb do scope "/", OutlookWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated] pipe_through [:browser, :redirect_if_user_is_authenticated, :check_registration]
live_session :redirect_if_user_is_authenticated, live_session :redirect_if_user_is_authenticated,
on_mount: [{OutlookWeb.UserAuth, :redirect_if_user_is_authenticated}] do on_mount: [{OutlookWeb.UserAuth, :redirect_if_user_is_authenticated}] do
@ -105,7 +115,7 @@ defmodule OutlookWeb.Router do
end end
scope "/", OutlookWeb do scope "/", OutlookWeb do
pipe_through [:browser, :public_root_layout] pipe_through :browser
delete "/users/log_out", UserSessionController, :delete delete "/users/log_out", UserSessionController, :delete
@ -114,8 +124,5 @@ defmodule OutlookWeb.Router do
live "/users/confirm/:token", UserConfirmationLive, :edit live "/users/confirm/:token", UserConfirmationLive, :edit
live "/users/confirm", UserConfirmationInstructionsLive, :new live "/users/confirm", UserConfirmationInstructionsLive, :new
end end
resources "/autoren", AutorController, only: [:index, :show]
resources "/artikel", ArtikelController, only: [:index, :show]
end end
end end

View File

@ -0,0 +1,21 @@
defmodule OutlookWeb.ViewHelpers do
import Phoenix.HTML, only: [raw: 1]
@doc "Just sanitize tags"
def tidy_raw(html) when is_binary(html) do
html
|> Floki.parse_fragment!()
|> Floki.raw_html()
|> raw
end
def tidy_raw(whatever) do
whatever
end
# TODO: implement (and use) the following function
@doc "Strip <a> tags to prevent broken html (or 'breaking') from user input."
def strip_links(html) do
raise "Yet to be implemented!"
end
end

View File

@ -56,6 +56,7 @@ defmodule Outlook.MixProject do
{:fast_html, "~> 2.0"}, {:fast_html, "~> 2.0"},
{:httpoison, "~> 1.8"}, {:httpoison, "~> 1.8"},
{:nanoid, "~> 2.0.5"}, {:nanoid, "~> 2.0.5"},
{:tz, "~> 0.24.0"},
] ]
end end

View File

@ -12,14 +12,14 @@
"ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"}, "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
"ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"}, "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
"elixir_make": {:hex, :elixir_make, "0.7.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"}, "elixir_make": {:hex, :elixir_make, "0.7.5", "784cc00f5fa24239067cc04d449437dcc5f59353c44eb08f188b2b146568738a", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "c3d63e8d5c92fa3880d89ecd41de59473fa2e83eeb68148155e25e8b95aa2887"},
"esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"}, "esbuild": {:hex, :esbuild, "0.6.1", "a774bfa7b4512a1211bf15880b462be12a4c48ed753a170c68c63b2c95888150", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "569f7409fb5a932211573fc20e2a930a0d5cf3377c5b4f6506c651b1783a1678"},
"expo": {:hex, :expo, "0.1.0", "d4e932bdad052c374118e312e35280f1919ac13881cb3ac07a209a54d0c81dd8", [:mix], [], "hexpm", "c22c536021c56de058aaeedeabb4744eb5d48137bacf8c29f04d25b6c6bbbf45"}, "expo": {:hex, :expo, "0.4.0", "bbe4bf455e2eb2ebd2f1e7d83530ce50fb9990eb88fc47855c515bfdf1c6626f", [:mix], [], "hexpm", "a8ed1683ec8b7c7fa53fd7a41b2c6935f539168a6bb0616d7fd6b58a36f3abf2"},
"fast_html": {:hex, :fast_html, "2.0.5", "c61760340606c1077ff1f196f17834056cb1dd3d5cb92a9f2cabf28bc6221c3c", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "605f4f4829443c14127694ebabb681778712ceecb4470ec32aa31012330e6506"}, "fast_html": {:hex, :fast_html, "2.0.5", "c61760340606c1077ff1f196f17834056cb1dd3d5cb92a9f2cabf28bc6221c3c", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "605f4f4829443c14127694ebabb681778712ceecb4470ec32aa31012330e6506"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"}, "finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
"gettext": {:hex, :gettext, "0.21.0", "15bbceb20b317b706a8041061a08e858b5a189654128618b53746bf36c84352b", [:mix], [{:expo, "~> 0.1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "04a66db4103b6d1d18f92240bb2c73167b517229316b7bef84e4eebbfb2f14f6"}, "gettext": {:hex, :gettext, "0.22.1", "e7942988383c3d9eed4bdc22fc63e712b655ae94a672a27e4900e3d4a2c43581", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "ad105b8dab668ee3f90c0d3d94ba75e9aead27a62495c101d94f2657a190ac5d"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"heroicons": {:hex, :heroicons, "0.5.2", "a7ae72460ecc4b74a4ba9e72f0b5ac3c6897ad08968258597da11c2b0b210683", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7ef96f455c1c136c335f1da0f1d7b12c34002c80a224ad96fc0ebf841a6ffef5"}, "heroicons": {:hex, :heroicons, "0.5.2", "a7ae72460ecc4b74a4ba9e72f0b5ac3c6897ad08968258597da11c2b0b210683", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7ef96f455c1c136c335f1da0f1d7b12c34002c80a224ad96fc0ebf841a6ffef5"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
@ -34,14 +34,14 @@
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.7.0-rc.2", "8faaff6f699aad2fe6a003c627da65d0864c868a4c10973ff90abfd7286c1f27", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "71abde2f67330c55b625dcc0e42bf76662dbadc7553c4f545c2f3759f40f7487"}, "phoenix": {:hex, :phoenix, "1.7.0", "cbed113bdc203e2ced75859011fe7e71eeebb6259cefa54de810d9c7048b5e22", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8526139d4bd79ec97c5c3c8e69f6cd663597f782756cec874ba7da5429c93e34"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.7", "2b5239bb3d7ceead2b3dc368a1dc588a91e55df46c4222441609018b2df151b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7027a71a3c94c3e2088d401f873f7a7adb045eea6c9493af1934389e30458e87"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.16", "781c6a3ac49e0451ca403848b40807171caea400896fe8ed8e5ddd6106ad5580", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "09e6ae2babe62f74bfcd1e3cac1a9b0e2c262557cc566300a843425c9cb6842a"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
@ -50,10 +50,11 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"}, "swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"}, "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
"tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"}, "tailwind": {:hex, :tailwind, "0.1.10", "21ed80ae1f411f747ee513470578acaaa1d0eb40170005350c5b0b6d07e2d624", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e0fc474dfa8ed7a4573851ac69c5fd3ca70fbb0a5bada574d1d657ebc6f2f1f1"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"tz": {:hex, :tz, "0.24.0", "a9073f152c5a9d0abeafde57150cd61d9f11faa7fa3710a20e8487ce05c76cee", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "5c08671bb10a56e09371106b08f5c9192449bb22e94a51de063c8c1317317027"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.4.3", "184ac396bdcd3dfceb5b74c17d221af659dd559a95b1b92041ecb51c9b728093", [:mix], [], "hexpm", "5e4dd85f305f43fd3d3e25d70bec4a45228dfed60f0f3b072d8eddff335539cf"}, "websock": {:hex, :websock, "0.4.3", "184ac396bdcd3dfceb5b74c17d221af659dd559a95b1b92041ecb51c9b728093", [:mix], [], "hexpm", "5e4dd85f305f43fd3d3e25d70bec4a45228dfed60f0f3b072d8eddff335539cf"},
"websock_adapter": {:hex, :websock_adapter, "0.4.5", "30038a3715067f51a9580562c05a3a8d501126030336ffc6edb53bf57d6d2d26", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.4", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "1d9812dc7e703c205049426fd4fe0852a247a825f91b099e53dc96f68bafe4c8"}, "websock_adapter": {:hex, :websock_adapter, "0.4.5", "30038a3715067f51a9580562c05a3a8d501126030336ffc6edb53bf57d6d2d26", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.4", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "1d9812dc7e703c205049426fd4fe0852a247a825f91b099e53dc96f68bafe4c8"},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,10 @@
function set_day_night_mode(){
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
set_day_night_mode()
matchMedia('(prefers-color-scheme: dark)').addEventListener("change", set_day_night_mode)

View File

@ -0,0 +1,131 @@
defmodule Outlook.PublicTest do
use Outlook.DataCase
# TODO: make this work
alias Outlook.Public
describe "artikel" do
alias Outlook.Public.Artikel
import Outlook.PublicFixtures
@invalid_attrs %{date: nil, public_content: nil, teaser: nil, title: nil, translator: nil, unauthorized: nil}
test "list_artikel/0 returns all artikel" do
artikel = artikel_fixture()
assert Artikel.list_artikel() == [artikel]
end
test "get_artikel!/1 returns the artikel with given id" do
artikel = artikel_fixture()
assert Artikel.get_artikel!(artikel.id) == artikel
end
test "create_artikel/1 with valid data creates a artikel" do
valid_attrs = %{date: "some date", public_content: "some public_content", teaser: "some teaser", title: "some title", translator: "some translator", unauthorized: true}
assert {:ok, %Artikel{} = artikel} = Artikel.create_artikel(valid_attrs)
assert artikel.date == "some date"
assert artikel.public_content == "some public_content"
assert artikel.teaser == "some teaser"
assert artikel.title == "some title"
assert artikel.translator == "some translator"
assert artikel.unauthorized == true
end
test "create_artikel/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Artikel.create_artikel(@invalid_attrs)
end
test "update_artikel/2 with valid data updates the artikel" do
artikel = artikel_fixture()
update_attrs = %{date: "some updated date", public_content: "some updated public_content", teaser: "some updated teaser", title: "some updated title", translator: "some updated translator", unauthorized: false}
assert {:ok, %Artikel{} = artikel} = Artikel.update_artikel(artikel, update_attrs)
assert artikel.date == "some updated date"
assert artikel.public_content == "some updated public_content"
assert artikel.teaser == "some updated teaser"
assert artikel.title == "some updated title"
assert artikel.translator == "some updated translator"
assert artikel.unauthorized == false
end
test "update_artikel/2 with invalid data returns error changeset" do
artikel = artikel_fixture()
assert {:error, %Ecto.Changeset{}} = Artikel.update_artikel(artikel, @invalid_attrs)
assert artikel == Artikel.get_artikel!(artikel.id)
end
test "delete_artikel/1 deletes the artikel" do
artikel = artikel_fixture()
assert {:ok, %Artikel{}} = Artikel.delete_artikel(artikel)
assert_raise Ecto.NoResultsError, fn -> Artikel.get_artikel!(artikel.id) end
end
test "change_artikel/1 returns a artikel changeset" do
artikel = artikel_fixture()
assert %Ecto.Changeset{} = Artikel.change_artikel(artikel)
end
end
describe "autoren" do
alias Outlook.Public.Autor
import Outlook.PublicFixtures
@invalid_attrs %{description: nil, homepage_name: nil, homepage_url: nil, name: nil}
test "list_autoren/0 returns all autoren" do
autor = autor_fixture()
assert Autoren.list_autoren() == [autor]
end
test "get_autor!/1 returns the autor with given id" do
autor = autor_fixture()
assert Autoren.get_autor!(autor.id) == autor
end
test "create_autor/1 with valid data creates a autor" do
valid_attrs = %{description: "some description", homepage_name: "some homepage_name", homepage_url: "some homepage_url", name: "some name"}
assert {:ok, %Autor{} = autor} = Autoren.create_autor(valid_attrs)
assert autor.description == "some description"
assert autor.homepage_name == "some homepage_name"
assert autor.homepage_url == "some homepage_url"
assert autor.name == "some name"
end
test "create_autor/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Autoren.create_autor(@invalid_attrs)
end
test "update_autor/2 with valid data updates the autor" do
autor = autor_fixture()
update_attrs = %{description: "some updated description", homepage_name: "some updated homepage_name", homepage_url: "some updated homepage_url", name: "some updated name"}
assert {:ok, %Autor{} = autor} = Autoren.update_autor(autor, update_attrs)
assert autor.description == "some updated description"
assert autor.homepage_name == "some updated homepage_name"
assert autor.homepage_url == "some updated homepage_url"
assert autor.name == "some updated name"
end
test "update_autor/2 with invalid data returns error changeset" do
autor = autor_fixture()
assert {:error, %Ecto.Changeset{}} = Autoren.update_autor(autor, @invalid_attrs)
assert autor == Autoren.get_autor!(autor.id)
end
test "delete_autor/1 deletes the autor" do
autor = autor_fixture()
assert {:ok, %Autor{}} = Autoren.delete_autor(autor)
assert_raise Ecto.NoResultsError, fn -> Autoren.get_autor!(autor.id) end
end
test "change_autor/1 returns a autor changeset" do
autor = autor_fixture()
assert %Ecto.Changeset{} = Autoren.change_autor(autor)
end
end
end

View File

@ -0,0 +1,86 @@
defmodule OutlookWeb.ArtikelControllerTest do
use OutlookWeb.ConnCase
# TODO: make this work
import Outlook.PublicFixtures
@create_attrs %{date: "some date", public_content: "some public_content", teaser: "some teaser", title: "some title", translator: "some translator", unauthorized: true}
@update_attrs %{date: "some updated date", public_content: "some updated public_content", teaser: "some updated teaser", title: "some updated title", translator: "some updated translator", unauthorized: false}
@invalid_attrs %{date: nil, public_content: nil, teaser: nil, title: nil, translator: nil, unauthorized: nil}
describe "index" do
test "lists all artikel", %{conn: conn} do
conn = get(conn, ~p"/artikel")
assert html_response(conn, 200) =~ "Listing Artikel"
end
end
describe "new artikel" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/artikel/new")
assert html_response(conn, 200) =~ "New Artikel"
end
end
describe "create artikel" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, ~p"/artikel", artikel: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/artikel/#{id}"
conn = get(conn, ~p"/artikel/#{id}")
assert html_response(conn, 200) =~ "Artikel #{id}"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/artikel", artikel: @invalid_attrs)
assert html_response(conn, 200) =~ "New Artikel"
end
end
describe "edit artikel" do
setup [:create_artikel]
test "renders form for editing chosen artikel", %{conn: conn, artikel: artikel} do
conn = get(conn, ~p"/artikel/#{artikel}/edit")
assert html_response(conn, 200) =~ "Edit Artikel"
end
end
describe "update artikel" do
setup [:create_artikel]
test "redirects when data is valid", %{conn: conn, artikel: artikel} do
conn = put(conn, ~p"/artikel/#{artikel}", artikel: @update_attrs)
assert redirected_to(conn) == ~p"/artikel/#{artikel}"
conn = get(conn, ~p"/artikel/#{artikel}")
assert html_response(conn, 200) =~ "some updated date"
end
test "renders errors when data is invalid", %{conn: conn, artikel: artikel} do
conn = put(conn, ~p"/artikel/#{artikel}", artikel: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Artikel"
end
end
describe "delete artikel" do
setup [:create_artikel]
test "deletes chosen artikel", %{conn: conn, artikel: artikel} do
conn = delete(conn, ~p"/artikel/#{artikel}")
assert redirected_to(conn) == ~p"/artikel"
assert_error_sent 404, fn ->
get(conn, ~p"/artikel/#{artikel}")
end
end
end
defp create_artikel(_) do
artikel = artikel_fixture()
%{artikel: artikel}
end
end

View File

@ -0,0 +1,86 @@
defmodule OutlookWeb.AutorControllerTest do
use OutlookWeb.ConnCase
# TODO: make this work
import Outlook.PublicFixtures
@create_attrs %{description: "some description", homepage_name: "some homepage_name", homepage_url: "some homepage_url", name: "some name"}
@update_attrs %{description: "some updated description", homepage_name: "some updated homepage_name", homepage_url: "some updated homepage_url", name: "some updated name"}
@invalid_attrs %{description: nil, homepage_name: nil, homepage_url: nil, name: nil}
describe "index" do
test "lists all autoren", %{conn: conn} do
conn = get(conn, ~p"/autoren")
assert html_response(conn, 200) =~ "Listing Autoren"
end
end
describe "new autor" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/autoren/new")
assert html_response(conn, 200) =~ "New Autor"
end
end
describe "create autor" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, ~p"/autoren", autor: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/autoren/#{id}"
conn = get(conn, ~p"/autoren/#{id}")
assert html_response(conn, 200) =~ "Autor #{id}"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/autoren", autor: @invalid_attrs)
assert html_response(conn, 200) =~ "New Autor"
end
end
describe "edit autor" do
setup [:create_autor]
test "renders form for editing chosen autor", %{conn: conn, autor: autor} do
conn = get(conn, ~p"/autoren/#{autor}/edit")
assert html_response(conn, 200) =~ "Edit Autor"
end
end
describe "update autor" do
setup [:create_autor]
test "redirects when data is valid", %{conn: conn, autor: autor} do
conn = put(conn, ~p"/autoren/#{autor}", autor: @update_attrs)
assert redirected_to(conn) == ~p"/autoren/#{autor}"
conn = get(conn, ~p"/autoren/#{autor}")
assert html_response(conn, 200) =~ "some updated description"
end
test "renders errors when data is invalid", %{conn: conn, autor: autor} do
conn = put(conn, ~p"/autoren/#{autor}", autor: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Autor"
end
end
describe "delete autor" do
setup [:create_autor]
test "deletes chosen autor", %{conn: conn, autor: autor} do
conn = delete(conn, ~p"/autoren/#{autor}")
assert redirected_to(conn) == ~p"/autoren"
assert_error_sent 404, fn ->
get(conn, ~p"/autoren/#{autor}")
end
end
end
defp create_autor(_) do
autor = autor_fixture()
%{autor: autor}
end
end

View File

@ -4,6 +4,8 @@ defmodule Outlook.ArticlesFixtures do
entities via the `Outlook.Articles` context. entities via the `Outlook.Articles` context.
""" """
# TODO: make this work
@doc """ @doc """
Generate a article. Generate a article.
""" """
@ -11,11 +13,17 @@ defmodule Outlook.ArticlesFixtures do
{:ok, article} = {:ok, article} =
attrs attrs
|> Enum.into(%{ |> Enum.into(%{
content: "some content", content: [%Outlook.InternalTree.InternalNode{name: "p", attributes: %{}, type: :element, nid: "54e8cedb-6459-4605-8301-367758675bb8", content: [
%Outlook.InternalTree.TranslationUnit{status: :untranslated, nid: "c0fcdf61-ae2d-482e-81b4-9b6e3baacd8b",
content: "A sentence with many letters <a href=\"dingsda.com\">and many, many <b>words. </b></a>"},
%Outlook.InternalTree.TranslationUnit{status: :untranslated, nid: "eac12d97-623d-4237-9f33-666298c7f494",
content: "<a href=\"dingsda.com\"><b>A</b> sentence</a> with many letters and many, many words. "}],
eph: %{sibling_with: :block}}],
date: ~U[2022-12-25 16:16:00Z], date: ~U[2022-12-25 16:16:00Z],
language: "some language", language: "EN",
title: "some title", title: "some title",
url: "some url" url: "some url",
author_id: 1
}) })
|> Outlook.Articles.create_article() |> Outlook.Articles.create_article()

View File

@ -0,0 +1,45 @@
defmodule Outlook.PublicFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Outlook.Public` context.
"""
# TODO: make this work
@doc """
Generate an artikel.
"""
def artikel_fixture(attrs \\ %{}) do
{:ok, artikel} =
attrs
|> Enum.into(%{
date: "some date",
public_content: "some public_content",
teaser: "some teaser",
title: "some title",
translator: "some translator",
unauthorized: true
})
|> Outlook.Public.create_artikel()
artikel
end
@doc """
Generate an autor.
"""
def autor_fixture(attrs \\ %{}) do
{:ok, autor} =
attrs
|> Enum.into(%{
description: "some description",
homepage_name: "some homepage_name",
homepage_url: "some homepage_url",
name: "some name"
})
|> Outlook.Public.create_autor()
autor
end
end

View File

@ -4,6 +4,8 @@ defmodule Outlook.TranslationsFixtures do
entities via the `Outlook.Translations` context. entities via the `Outlook.Translations` context.
""" """
# TODO: make this work
@doc """ @doc """
Generate a translation. Generate a translation.
""" """
@ -17,7 +19,8 @@ defmodule Outlook.TranslationsFixtures do
public: true, public: true,
teaser: "some teaser", teaser: "some teaser",
title: "some title", title: "some title",
unauthorized: true unauthorized: true,
article_id: 1
}) })
|> Outlook.Translations.create_translation() |> Outlook.Translations.create_translation()