Compare commits

61 Commits

Author SHA1 Message Date
2111cfa1d1 Add iframe to elements that may be empty 2025-05-05 14:45:43 +02:00
c8d1639afa Fix ugly bug 2023-06-09 13:57:05 +02:00
43e11178db Fix darkmode-widget cover buttons 2023-06-08 16:33:03 +02:00
4828a68bcd Fix following links when editing translation 2023-05-29 13:19:42 +02:00
2b25c13095 Fix bug which omits element's attributes
There is a questionable design decision: for
getting the attributes from the %InternalNode{} into
the rendered html output they always have to be copied
into node.eph.attributes. (OutlookWeb.HtmlDocComponent.dnode())
2023-05-28 15:31:08 +02:00
357bcae450 Add ’ to partitioning regex 2023-05-28 15:23:25 +02:00
698da7f8b7 Add toggling fields in translation form 2023-05-04 18:14:44 +02:00
d53eceb7a0 Add remarks to articles 2023-05-04 18:12:57 +02:00
888e7575f0 Add remarks field to translations 2023-05-04 18:11:40 +02:00
8a0e2f22c1 Improve html tree component 2023-05-03 22:37:43 +02:00
7990b74bf0 Add "Unite with next" button 2023-05-02 22:09:04 +02:00
4a8f1c3c80 Update add_phx_click_event making :target option optional 2023-05-02 21:59:27 +02:00
7e085f5202 Update layout 2023-03-30 14:31:10 +02:00
3b8b1b19f0 Cleanup TunitModifications 2023-03-28 23:44:19 +02:00
5ac1a6c8e7 Remove superfluous line 2023-03-28 14:51:57 +02:00
3a2af2adb4 Add TunitModifications with first modifier unite_with_next 2023-03-28 14:48:26 +02:00
b92503d643 Replace outdated render_html_preview() with render_doc component 2023-03-15 13:24:54 +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
71 changed files with 1306 additions and 289 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,TunitModifications}
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,10 +1,49 @@
.main {
@apply place-content-center;
}
.article { .article {
/* @apply pr-8 */ /* @apply pr-8 */
max-width: 25rem; max-width: 25rem;
} }
.article .tunit { .article span.tunit {
@apply hover:bg-gray-300; @apply hover:bg-gray-300;
padding: 5px 0;
margin: -5px 0;
}
.dark .article span.tunit {
@apply hover:bg-gray-700;
padding: 5px 0;
margin: -5px 0;
}
.article span.tunit[current="yes"], .article span.tunit[selected="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 {
@ -28,7 +67,11 @@
} }
.article h4 { .article h4 {
@apply my-2 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 {
@ -47,6 +90,10 @@
@apply text-cyan-900; @apply text-cyan-900;
} }
.dark .article a {
@apply text-cyan-700;
}
.article ul { .article ul {
@apply pl-6 list-disc my-2; @apply pl-6 list-disc my-2;
} }

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,54 @@
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")
let article_preview_links = document.querySelectorAll(".article-preview a")
article_preview_links.forEach(el => el.addEventListener('click', e => e.preventDefault()))
},
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",

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

@ -12,6 +12,7 @@ defmodule Outlook.Articles.Article do
field :language, :string, default: "EN" field :language, :string, default: "EN"
field :title, :string field :title, :string
field :url, :string field :url, :string
field :remarks, :string
belongs_to :author, Author belongs_to :author, Author
has_many :translations, Translation, on_delete: :delete_all has_many :translations, Translation, on_delete: :delete_all
@ -21,7 +22,7 @@ defmodule Outlook.Articles.Article do
@doc false @doc false
def changeset(article, attrs) do def changeset(article, attrs) do
article article
|> cast(attrs, [:title, :content, :url, :language, :date, :author_id]) |> cast(attrs, [:title, :content, :url, :language, :date, :author_id, :remarks])
|> validate_required([:title, :content, :url, :language, :date, :author_id]) |> validate_required([:title, :content, :url, :language, :date, :author_id])
|> foreign_key_constraint(:author_id) |> foreign_key_constraint(:author_id)
end end

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

@ -63,7 +63,7 @@ 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) ]
@ -73,7 +73,7 @@ defmodule Outlook.HtmlPreparations.HtmlPreparation 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)]

View File

@ -1,6 +1,6 @@
defmodule Outlook.InternalTree do defmodule Outlook.InternalTree do
alias Outlook.InternalTree.{Html,Modifiers,RawInternalBasic,InternalTree,Translation} alias Outlook.InternalTree.{Html,Modifiers,RawInternalBasic,InternalTree,Translation,TunitModifications}
alias Outlook.HtmlPreparations.HtmlPreparation alias Outlook.HtmlPreparations.HtmlPreparation
alias Outlook.{Hyphenation, Translations} alias Outlook.{Hyphenation, Translations}
@ -10,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
@ -31,9 +26,12 @@ defmodule Outlook.InternalTree do
def add_phx_click_event(tree, opts) do def add_phx_click_event(tree, opts) do
phx_opts = %{ phx_opts = %{
"phx-click": Keyword.get(opts, :click), "phx-click": Keyword.get(opts, :click),
"phx-target": Keyword.get(opts, :target) |> to_string,
"phx-value-nid": fn n -> n.nid end "phx-value-nid": fn n -> n.nid end
} }
phx_opts = case Keyword.has_key?(opts, :target) do
true -> Map.put(phx_opts, "phx-target", Keyword.get(opts, :target) |> to_string)
false -> phx_opts
end
options = Map.put(%{}, Keyword.get(opts, :nodes, :elements), phx_opts) options = Map.put(%{}, Keyword.get(opts, :nodes, :elements), phx_opts)
garnish(tree, options) garnish(tree, options)
end end
@ -58,6 +56,7 @@ defmodule Outlook.InternalTree do
def render_public_content(tree, translation, language) do def render_public_content(tree, translation, language) do
Translation.render_translation(tree, translation) Translation.render_translation(tree, translation)
|> garnish(%{})
|> Html.render_doc() |> Html.render_doc()
|> Hyphenation.hyphenate(language) |> Hyphenation.hyphenate(language)
end end
@ -74,6 +73,13 @@ defmodule Outlook.InternalTree do
def get_tunit_ids(tree) do def get_tunit_ids(tree) do
InternalTree.collect_tunit_ids(tree) InternalTree.collect_tunit_ids(tree)
# |> List.flatten() end
def modify_tunits(tree, modifier, tu_ids) do
TunitModifications.apply_modifier(tree, modifier, tu_ids)
end
def tunit_modifiers() do
TunitModifications.modifiers()
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

View File

@ -10,7 +10,7 @@ defmodule Outlook.InternalTree.RawInternalBasic do
@splitmarker "@@translationunit@@" @splitmarker "@@translationunit@@"
@nonperiodmarker "@@nonperiod@@" @nonperiodmarker "@@nonperiod@@"
@void_elements ~w(img br hr) @void_elements ~w(img br hr iframe)
def set_split_markers([ %InternalNode{type: :text} = textnode | rest ]) do def set_split_markers([ %InternalNode{type: :text} = textnode | rest ]) do
[ %InternalNode{textnode | [ %InternalNode{textnode |
@ -18,7 +18,7 @@ defmodule Outlook.InternalTree.RawInternalBasic do
|> String.replace(~r/\.\.\.+/u, "") |> String.replace(~r/\.\.\.+/u, "")
|> String.replace(~r/([[:upper:]])\./u, "\\1#{@nonperiodmarker}") |> String.replace(~r/([[:upper:]])\./u, "\\1#{@nonperiodmarker}")
|> String.replace(~r/(\d)\.(\d)/u, "\\1#{@nonperiodmarker}\\2") |> String.replace(~r/(\d)\.(\d)/u, "\\1#{@nonperiodmarker}\\2")
|> String.replace(~r|([.?!]["'”]?\s*)|u, "\\1#{@splitmarker}") |> String.replace(~r|([.?!]["'”]?\s*)|u, "\\1#{@splitmarker}")
|> String.replace(@nonperiodmarker, ".") |> String.replace(@nonperiodmarker, ".")
} | set_split_markers(rest) ] } | set_split_markers(rest) ]
end end

View File

@ -0,0 +1,82 @@
defmodule Outlook.InternalTree.TunitModifications do
alias Outlook.InternalTree.{InternalNode,TranslationUnit}
def modifiers do
[
%{
name: "unite_with_next",
fn: &unite_with_next/2,
label: "Unite with next",
description: "unite selected translation unit with (unselected) next"
},
# %{
# name: "split_tunit",
# fn: &split_tunit/2,
# label: "Split Translation unit",
# description: "split translation unit into two"
# }
]
end
# Modifier functions
defp unite_with_next(nodelist, tu_ids) when is_list(tu_ids) do
ids_to_process = Enum.reverse(tu_ids)
Enum.reduce(ids_to_process, nodelist, fn id, nodes -> unite_with_next(nodes, id) end)
end
defp unite_with_next(nodelist, tu_id) do
ind = Enum.find_index(nodelist, fn n -> n.nid == tu_id end)
nodes = Enum.slice(nodelist, ind, 2)
unite_with_next(nodelist, ind, nodes)
end
defp unite_with_next(nodelist, ind, [unit,next]) do
nunit = %TranslationUnit{unit | content: unit.content <> next.content}
nodelist
|> List.replace_at(ind, nunit)
|> List.delete_at(ind + 1)
end
defp unite_with_next(nodelist, _, [_]) do
nodelist
end
defp split_tunit(_nodelist, _tu_ids) do
end
# Function and helpers to apply modifiers
def apply_modifier([ %InternalNode{} = node | rest ], modifier, tu_ids) when node.type == :element do
content = case List.first(node.content) do
%TranslationUnit{} -> process_tunit_list(node.content, modifier, tu_ids)
_ -> apply_modifier(node.content, modifier, tu_ids)
end
[%InternalNode{node| content: content} | apply_modifier(rest, modifier, tu_ids)]
end
def apply_modifier([node | rest], modifier, tu_ids), do: [node | apply_modifier(rest, modifier, tu_ids)]
def apply_modifier([],_, _), do: []
def process_tunit_list(tunits, modifier, tu_ids) do
modi_fun = get_modi_fun(modifier)
ids_to_process = get_ids_to_process(tunits, tu_ids)
case length(ids_to_process) do
0 -> tunits
_ -> modi_fun.(tunits, ids_to_process)
end
end
@doc false
def get_ids_to_process(tunits, tu_ids) do
present_ids = Enum.map(tunits, fn u -> u.nid end)
found_ids = MapSet.new(present_ids)
|> MapSet.intersection(MapSet.new(tu_ids))
|> MapSet.to_list()
# make sure to return ids in the order they occur in tunits
Enum.filter(present_ids, fn pres -> pres in found_ids end)
end
defp get_modi_fun(modifier) do
modifiers()
|> Enum.find(fn m -> m.name == modifier end)
|> Map.get(:fn)
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

@ -15,6 +15,7 @@ defmodule Outlook.Translations.Translation do
field :public_content, :string field :public_content, :string
field :title, :string field :title, :string
field :unauthorized, :boolean, default: false field :unauthorized, :boolean, default: false
field :remarks, :string
belongs_to :user, User belongs_to :user, User
belongs_to :article, Article belongs_to :article, Article
@ -24,7 +25,7 @@ defmodule Outlook.Translations.Translation do
@doc false @doc false
def changeset(translation, attrs) do def changeset(translation, attrs) do
translation translation
|> cast(attrs, [:language, :title, :teaser, :date, :public, :unauthorized, :article_id, :public_content]) |> cast(attrs, [:language, :title, :teaser, :date, :public, :unauthorized, :article_id, :public_content, :remarks])
|> cast(attrs, [:content]) |> cast(attrs, [:content])
|> validate_required([:language, :title, :content, :date, :public, :unauthorized, :article_id]) |> validate_required([:language, :title, :content, :date, :public, :unauthorized, :article_id])
|> unique_constraint([:language, :article_id], |> unique_constraint([:language, :article_id],

View File

@ -66,6 +66,11 @@ 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
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 +83,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,10 +54,12 @@ 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} ->
Logger.debug "Deepl response: #{response |> inspect}"
steps = Map.get(response, :seconds_remaining, 1) * 5 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}})

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,10 +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.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="relative 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 mt-5 ml-40 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

@ -3,6 +3,7 @@ defmodule OutlookWeb.HtmlTreeComponent do
use Phoenix.Component use Phoenix.Component
# use OutlookWeb, :html # use OutlookWeb, :html
import OutlookWeb.CoreComponents import OutlookWeb.CoreComponents
import OutlookWeb.ViewHelpers
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
@ -15,10 +16,14 @@ defmodule OutlookWeb.HtmlTreeComponent do
end end
def attributes(assigns) do def attributes(assigns) do
~H"&nbsp; <%= @name %>=&quot;<%= @value %>&quot;" ~H"&nbsp; <%= @name %>=&quot;<%= elipsed_text(@value, 16) %>&quot;"
end 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(%{node: %{status: _}} = assigns) do
~H"""
<span title={@node.content} {@node.eph.attributes}><%= elipsed_text(@node.content, 30) %></span><br>
"""
end
def tnode(assigns) when assigns.node.type == :element do def tnode(assigns) when assigns.node.type == :element do
~H""" ~H"""
@ -32,7 +37,7 @@ defmodule OutlookWeb.HtmlTreeComponent do
def tnode(assigns) when assigns.node.type == :text do def tnode(assigns) when assigns.node.type == :text do
~H""" ~H"""
"<%= String.slice(@node.content, 0..35) %><%= if String.length(@node.content) > 35 do %>..."<% end %><br> "<%= elipsed_text(@node.content, 30) %><br>
""" """
end end

View File

@ -1,42 +1,9 @@
<header class="px-4 sm:px-6 lg:px-8"> <header class="">
<div class="flex items-center justify-between border-b border-zinc-100 py-3"> <.breakpoint_indicator :if={Mix.env == :dev} />
<div class="flex items-center gap-4"> <.dark_mode_widget />
<a href="/">
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
<path
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
fill="#FD4F00"
/>
</svg>
</a>
<p class="rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand">
v1.7
</p>
</div>
<div class="flex items-center gap-4">
<a
href="https://twitter.com/elixirphoenix"
class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
@elixirphoenix
</a>
<a
href="https://github.com/phoenixframework/phoenix"
class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
</header> </header>
<main class="px-4 py-20 sm:px-6 lg:px-8"> <main class="px-4 sm:px-6 lg:px-8 lg:mx-auto h-screen">
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl h-fit">
<.flash kind={:info} title="Success!" flash={@flash} /> <.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} /> <.flash kind={:error} title="Error!" flash={@flash} />
<.flash <.flash

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

@ -1,11 +1,15 @@
<header class=""> <header class="">
<.breakpoint_indicator :if={Mix.env == :dev} />
<a href="/"> <a href="/">
<img class="w-full" src="/images/elbefoto-lg.jpg" <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"> 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> </a>
<.dark_mode_widget />
</header> </header>
<main class="px-4 py-20 sm:px-6 lg:px-8"> <main class="px-2 sm:px-6 lg:px-8">
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-xl">
<.flash kind={:info} title="Success!" flash={@flash} /> <.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} /> <.flash kind={:error} title="Error!" flash={@flash} />
<%= @inner_content %> <%= @inner_content %>

View File

@ -7,12 +7,13 @@
<.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 class="absolute">
<%= if @current_user do %> <%= if @current_user do %>
<li> <li>
<%= @current_user.email %> <%= @current_user.email %>

View File

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

View File

@ -9,12 +9,12 @@ defmodule OutlookWeb.TunitEditorComponent do
def tunit_editor(assigns) do def tunit_editor(assigns) do
~H""" ~H"""
<div id="translation-unit-editor" phx-nohook="tunit_editor"> <div id="translation-unit-editor" phx-no-hook="tunit_editor" phx-target={@target}>
<%!-- <div class="h-48 p-2 border border-slate-500 rounded" contenteditable phx-no-change="update_current_tunit"> <%!-- <div class="h-48 p-2 border border-slate-500 rounded" contenteditable phx-no-change="update_current_tunit">
<%= @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-96 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} />
@ -24,7 +24,7 @@ defmodule OutlookWeb.TunitEditorComponent do
defp statuses() do defp statuses() do
[ {:untranslated, "bg-red-800"}, [ {:untranslated, "bg-red-800"},
{:passable, "bg-amber-500"}, {:passable, "bg-amber-500/70"},
{:done, "bg-green-700"} ] {:done, "bg-green-700"} ]
end end

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,17 +1,17 @@
<header class="mb-20"> <header class="mb-6">
<h1 class="text-lg font-semibold leading-8 text-zinc-800"><%= @artikel.title %></h1> <h1 class="text-lg font-semibold leading-tight text-stone-800 dark:text-stone-200"><%= @artikel.title %></h1>
<p class="my-2"><.link href={"/autoren/#{@artikel.article.author.id}"}><%= @artikel.article.author.name %></.link> <p class="my-2"><.link href={~p"/autoren/#{@artikel.autor_id}"}><%= @artikel.autor_name %></.link>
&nbsp;&nbsp;&nbsp; — &nbsp;&nbsp;&nbsp;<%= Calendar.strftime(@artikel.article.date, "%d.%m.%Y") %></p> &nbsp;&nbsp;&nbsp; — &nbsp;&nbsp;&nbsp;<%= Calendar.strftime(@artikel.date_org, "%d.%m.%Y") %></p>
<div>Original <div>Original Artikel:
<.link href={@artikel.article.url} > <.link class="hover:text-sky-700" href={@artikel.url_org} >
<%= @artikel.article.title %> <%= @artikel.title_org %>
</.link><br> </.link><br>
</div> </div>
<div class="my-2"> <div class="my-2">
Übersetzung <%= Calendar.strftime(@artikel.date, "%d.%m.%Y") %> Übersetzung: <%= Calendar.strftime(@artikel.date, "%d.%m.%Y") %>
</div> </div>
</header> </header>
<div class="article w-full"><%= @artikel.public_content |> raw %></div> <div class="article w-full mx-auto max-w-xs"><%= @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,13 +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 %>
<.artikel :for={translation <- article.translations} artikel={translation} show_author={false} />
<% end %>
<.back navigate={~p"/autoren"}>Back to autoren</.back> <.back navigate={~p"/autoren"}>Back to autoren</.back>

View File

@ -26,6 +26,7 @@ defmodule OutlookWeb.ArticleLive.FormComponent do
<.input field={{f, :language}} type="select" label="language" <.input field={{f, :language}} type="select" label="language"
options={Application.get_env(:outlook,:deepl)[:source_langs]} /> options={Application.get_env(:outlook,:deepl)[:source_langs]} />
<.input field={{f, :date}} type="datetime-local" label="date" /> <.input field={{f, :date}} type="datetime-local" label="date" />
<.input field={{f, :remarks}} type="textarea" label="remarks" class="h-28" />
<:actions> <:actions>
<.button phx-disable-with="Saving...">Save Article</.button> <.button phx-disable-with="Saving...">Save Article</.button>
</:actions> </:actions>

View File

@ -0,0 +1,24 @@
defmodule OutlookWeb.ArticleLive.MenuComponent do
use OutlookWeb, :live_component
attr :entries, :list
# attr :target, :integer, default: 0 # @myself of the Live(View|Component) where the handlers reside
attr :handler, :string
@impl true
def render(assigns) do
~H"""
<div>
<.menu_item :for={entry <- @entries} entry={entry} handler={@handler} />
</div>
"""
end
def menu_item(assigns) do
~H"""
<div title={@entry.description} phx-click={@handler} phx-value-modifier={@entry.name}>
<%= @entry.label %>
</div>
"""
end
end

View File

@ -17,6 +17,7 @@ defmodule OutlookWeb.ArticleLive.New do
|> assign(:raw_html_input, %RawHtmlInput{}) |> assign(:raw_html_input, %RawHtmlInput{})
|> assign(:changeset, Articles.change_raw_html_input(%RawHtmlInput{})) |> assign(:changeset, Articles.change_raw_html_input(%RawHtmlInput{}))
|> assign(:selected_els, []) |> assign(:selected_els, [])
|> assign(:selected_tunits, [])
|> assign(:step, :import_raw_html)} |> assign(:step, :import_raw_html)}
end end
@ -26,6 +27,7 @@ defmodule OutlookWeb.ArticleLive.New do
article = %Article{author_id: author.id} article = %Article{author_id: author.id}
{:noreply, {:noreply,
socket socket
|> assign(:menu_entries, InternalTree.tunit_modifiers()) # REMOVE ME!
|> assign(:author, author) |> assign(:author, author)
|> assign(:article, article)} |> assign(:article, article)}
end end
@ -43,7 +45,9 @@ defmodule OutlookWeb.ArticleLive.New do
{:noreply, {:noreply,
socket socket
|> assign(:raw_internal_tree, |> assign(:raw_internal_tree,
HtmlPreparations.convert_raw_html_input(raw_html_input_params["content"])) HtmlPreparations.convert_raw_html_input(raw_html_input_params["content"])
|> InternalTree.garnish(%{})
)
|> assign(:step, :review_raw_internaltree)} |> assign(:step, :review_raw_internaltree)}
false -> false ->
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
@ -54,10 +58,42 @@ defmodule OutlookWeb.ArticleLive.New do
def handle_event("approve_raw_internaltree", _, socket) do def handle_event("approve_raw_internaltree", _, socket) do
socket = socket socket = socket
|> assign(:raw_internal_tree, |> assign(:raw_internal_tree,
InternalTree.partition_text(socket.assigns.raw_internal_tree)) InternalTree.partition_text(socket.assigns.raw_internal_tree)
|> InternalTree.garnish(%{tunits: %{class: :tunit}})
|> InternalTree.add_phx_click_event(
nodes: :tunits,
click: "toggle_selected_tunit")
)
|> assign(:menu_entries, InternalTree.tunit_modifiers())
{:noreply, socket |> assign(:step, :review_translation_units)} {:noreply, socket |> assign(:step, :review_translation_units)}
end end
@impl true
def handle_event("toggle_selected_tunit", %{"nid" => tunit_id}, socket) do
{:noreply, toggle_selected_tunit(socket, tunit_id)}
end
defp toggle_selected_tunit(socket, tunit_id) do
before = socket.assigns.selected_tunits
selected_tunits = case Enum.member?(before, tunit_id) do
false -> List.insert_at(before, -1, tunit_id)
true -> List.delete(before, tunit_id)
end
fun = fn n -> n.nid in selected_tunits && "yes" || "no" end
socket
|> assign(:raw_internal_tree, InternalTree.garnish(socket.assigns.raw_internal_tree, %{tunits: %{selected: fun}}))
|> assign(:selected_tunits, selected_tunits)
end
@impl true
def handle_event("modify_tunits", %{"modifier" => modifier}, socket) do
{:noreply,
socket
|> assign(:raw_internal_tree,
InternalTree.modify_tunits(socket.assigns.raw_internal_tree, modifier, socket.assigns.selected_tunits)
)}
end
@impl true @impl true
def handle_event("approve_translation_units", _, socket) do def handle_event("approve_translation_units", _, socket) do
{:noreply, socket |> assign(:step, :final_form)} {:noreply, socket |> assign(:step, :final_form)}

View File

@ -4,7 +4,7 @@
<.import_raw_html :if={@step == :import_raw_html} changeset={@changeset} /> <.import_raw_html :if={@step == :import_raw_html} changeset={@changeset} />
<.review_raw_internaltree :if={@step == :review_raw_internaltree} raw_internal_tree={@raw_internal_tree} /> <.review_raw_internaltree :if={@step == :review_raw_internaltree} raw_internal_tree={@raw_internal_tree} />
<.review_translation_units :if={@step == :review_translation_units} /> <.review_translation_units :if={@step == :review_translation_units} raw_internal_tree={@raw_internal_tree} menu_entries={@menu_entries} />
<.live_component <.live_component
:if={@step == :final_form} :if={@step == :final_form}
module={OutlookWeb.ArticleLive.FormComponent} module={OutlookWeb.ArticleLive.FormComponent}

View File

@ -31,7 +31,7 @@ defmodule OutlookWeb.ArticleLive.NewComponents do
<div>Review Raw InternalTree</div> <div>Review Raw InternalTree</div>
<div class="flex"> <div class="flex">
<div id="html-preview" class="article"> <div id="html-preview" class="article">
<%= InternalTree.render_html_preview(@raw_internal_tree) |> raw %> <.render_doc tree={@raw_internal_tree} ></.render_doc>
</div> </div>
<div id="html-tree"> <div id="html-tree">
<.render_tree tree={@raw_internal_tree} ></.render_tree> <.render_tree tree={@raw_internal_tree} ></.render_tree>
@ -44,6 +44,17 @@ defmodule OutlookWeb.ArticleLive.NewComponents do
def review_translation_units(assigns) do def review_translation_units(assigns) do
~H""" ~H"""
<div>Review Translation Units</div> <div>Review Translation Units</div>
<div class="flex gap-4">
<div id="html-tree" class="article w-96 overflow-auto whitespace-nowrap">
<.render_tree tree={@raw_internal_tree} ></.render_tree>
</div>
<div id="partition-preview" class="article show-boundary overflow-auto h-full">
<.render_doc tree={@raw_internal_tree} ></.render_doc>
</div>
<div>
<.live_component module={OutlookWeb.ArticleLive.MenuComponent} entries={@menu_entries} handler="modify_tunits" id={:review_tunits} />
</div>
</div>
<.button phx-click="approve_translation_units">Continue</.button> <.button phx-click="approve_translation_units">Continue</.button>
""" """
end end

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,15 @@
<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="my-4">
<div>Remarks:</div>
<%= @article.remarks |> tidy_raw %>
</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,12 +8,13 @@ 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>
</.header> </.header>
<div phx-click={JS.toggle(to: ".more-fields")} class="cursor-pointer">more/less fields</div>
<.simple_form <.simple_form
:let={f} :let={f}
for={@changeset} for={@changeset}
@ -23,29 +24,34 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
phx-submit="save" phx-submit="save"
> >
<.input field={{f, :article_id}} type="hidden" /> <.input field={{f, :article_id}} type="hidden" />
<.input field={{f, :language}} type="select" label="language" <div class="more-fields">
options={Application.get_env(:outlook,:deepl)[:target_langs]} /> <.input field={{f, :language}} type="select" label="language"
options={Application.get_env(:outlook,:deepl)[:target_langs]} />
</div>
<.input field={{f, :title}} type="text" label="title" /> <.input field={{f, :title}} type="text" label="title" />
<.input field={{f, :teaser}} type="textarea" label="teaser" class="h-28" /> <div class="more-fields">
<.input field={{f, :date}} type="datetime-local" label="date" /> <.input field={{f, :teaser}} type="textarea" label="teaser" class="h-28" />
<div class="flex items-center justify-between"> <.input field={{f, :date}} type="datetime-local" label="date" />
<.input field={{f, :public}} type="checkbox" label="public" /> <div class="flex items-center justify-between">
<.input field={{f, :unauthorized}} type="checkbox" label="unauthorized" /> <.input field={{f, :public}} type="checkbox" label="public" />
<.input field={{f, :unauthorized}} type="checkbox" label="unauthorized" />
</div>
<.input field={{f, :remarks}} type="textarea" label="remarks" class="h-28" />
</div> </div>
<input type="hidden" id="continue_edit" name="continue_edit" value="false" /> <input type="hidden" id="continue_edit" name="continue_edit" value="false" />
<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} />
</div> </div>
<div class="article basis-1/2 max-h-screen overflow-auto"> <div class="article article-preview basis-1/2 max-h-screen overflow-auto">
<.button phx-disable-with="Translating..." phx-click="translate-deepl" phx-target={@myself} <.button phx-disable-with="Translating..." phx-click="translate-deepl" phx-target={@myself}
data-confirm-not="Are you sure? All previously translated text will be lost.">Translate with Deepl</.button> data-confirm-not="Are you sure? All previously translated text will be lost.">Translate with Deepl</.button>
<progress :if={@deepl_progress} max="100" value={@deepl_progress} /> <progress :if={@deepl_progress} max="100" value={@deepl_progress} />
@ -63,6 +69,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 +125,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 +175,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
@ -186,6 +216,7 @@ defmodule OutlookWeb.TranslationLive.FormComponent do
defp continue_edit(socket, :edit, %{"continue_edit" => "true"}) do defp continue_edit(socket, :edit, %{"continue_edit" => "true"}) do
socket socket
|> assign(:translation, Translations.get_translation!(socket.assigns.translation.id))
end end
defp continue_edit(socket, :new, %{"continue_edit" => "true"} = params) do defp continue_edit(socket, :new, %{"continue_edit" => "true"} = params) do
socket |> push_patch(to: ~p(/translations/#{params["id"]}/edit)) socket |> push_patch(to: ~p(/translations/#{params["id"]}/edit))

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

@ -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

@ -11,15 +11,16 @@
<.list> <.list>
<:item title="Language"><%= @translation.language %></:item> <:item title="Language"><%= @translation.language %></:item>
<:item title="Title"><%= @translation.title %></:item> <:item title="Title"><%= @translation.title %></:item>
<:item title="Teaser"><%= @translation.teaser %></:item> <:item title="Teaser"><%= @translation.teaser |> tidy_raw %></:item>
<%!-- <:item title="Content"><%= @translation.content %></:item> --%> <%!-- <:item title="Content"><%= @translation.content %></:item> --%>
<:item title="Date"><%= @translation.date %></:item> <:item title="Date"><%= @translation.date %></:item>
<:item title="Public"><%= @translation.public %></:item> <:item title="Public"><%= @translation.public %></:item>
<:item title="Unauthorized"><%= @translation.unauthorized %></:item> <:item title="Unauthorized"><%= @translation.unauthorized %></:item>
<:item title="Remarks"><%= @translation.remarks |> tidy_raw %></: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

@ -34,7 +34,7 @@ defmodule OutlookWeb.Router do
get "/", ArtikelController, :index get "/", ArtikelController, :index
resources "/autoren", AutorController, only: [:index, :show] resources "/autoren", AutorController, only: [:index, :show]
resources "/artikel", ArtikelController, 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.

View File

@ -0,0 +1,30 @@
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
def elipsed_text(text, length) do
if String.length(text) < length do
text
else
part_length = (length - 3) / 2 |> trunc()
"#{String.slice(text, 0..part_length)}#{String.slice(text, -part_length..-1)}"
end
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,7 +50,7 @@
"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"},

View File

@ -0,0 +1,9 @@
defmodule Outlook.Repo.Migrations.AddRemarksToTranslations do
use Ecto.Migration
def change do
alter table(:translations) do
add :remarks, :text
end
end
end

View File

@ -0,0 +1,9 @@
defmodule Outlook.Repo.Migrations.AddRemarksToArticles do
use Ecto.Migration
def change do
alter table(:articles) do
add :remarks, :text
end
end
end

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: 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,83 @@
defmodule Outlook.InternalTreeTest do
use Outlook.DataCase
# import Outlook.InternalTreeTestHelpers
describe "internal_tree" do
alias Outlook.InternalTree
alias Outlook.InternalTree.{InternalNode,TranslationUnit}
def tree() do
[
%InternalNode{
name: "p",
attributes: %{},
type: :element,
nid: "rRIib2h8tyix",
content: [
%TranslationUnit{status: :untranslated, nid: "GuU9v6xeSS7e", content: "Joe Biden a.", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "bzCLsYGNe2PG", content: "k.", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "GyRUrzwH9LcP", content: "a. ", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "y2yb38U4hkya", content: "Crash Test Dummy.", eph: %{}}
],
eph: %{sibling_with: :block}
}
]
end
test "unite_with_next unites with next in simple case" do
assert InternalTree.modify_tunits(tree(), "unite_with_next", ["bzCLsYGNe2PG"]) == [
%InternalNode{
name: "p",
attributes: %{},
type: :element,
nid: "rRIib2h8tyix",
content: [
%TranslationUnit{status: :untranslated, nid: "GuU9v6xeSS7e", content: "Joe Biden a.", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "bzCLsYGNe2PG", content: "k.a. ", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "y2yb38U4hkya", content: "Crash Test Dummy.", eph: %{}}
],
eph: %{sibling_with: :block}
}
]
end
test "unite_with_next unites all with next in complex case" do
assert InternalTree.modify_tunits(tree(), "unite_with_next", ["GuU9v6xeSS7e","bzCLsYGNe2PG","GyRUrzwH9LcP"]) == [
%InternalNode{
name: "p",
attributes: %{},
type: :element,
nid: "rRIib2h8tyix",
content: [
%TranslationUnit{
status: :untranslated,
nid: "GuU9v6xeSS7e",
content: "Joe Biden a.k.a. Crash Test Dummy.",
eph: %{}
}
],
eph: %{sibling_with: :block}
}
]
end
test "unite_with_next ignores id if there is no next tunit" do
assert InternalTree.modify_tunits(tree(), "unite_with_next", ["y2yb38U4hkya"]) == [
%InternalNode{
name: "p",
attributes: %{},
type: :element,
nid: "rRIib2h8tyix",
content: [
%TranslationUnit{status: :untranslated, nid: "GuU9v6xeSS7e", content: "Joe Biden a.", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "bzCLsYGNe2PG", content: "k.", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "GyRUrzwH9LcP", content: "a. ", eph: %{}},
%TranslationUnit{status: :untranslated, nid: "y2yb38U4hkya", content: "Crash Test Dummy.", eph: %{}}
],
eph: %{sibling_with: :block}
}
]
end
end
end

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()