18 Commits

Author SHA1 Message Date
24b0942a13 grover without dynamicsupervisor? 2024-08-13 09:48:50 +02:00
256c883488 nicer pagination 2024-08-12 16:52:26 +02:00
6d64449c1b fix last page calculation 2024-08-12 16:52:01 +02:00
6751ade1d9 fix empty new_posts count 2024-08-12 16:37:50 +02:00
bf7f1a4601 update deps 2024-08-12 16:27:36 +02:00
bab66c8079 fix attachment links 2024-08-12 16:19:22 +02:00
914246f08a add credo 2024-08-12 16:19:06 +02:00
0c52c18d92 Merge branch 'develop' 2024-08-12 15:52:28 +02:00
cb66d127b6 remove plausible 2024-08-12 15:52:14 +02:00
ce5a011d90 Merge branch 'feature/meeseeks-parser' into develop 2024-08-12 15:50:41 +02:00
fede012e44 meeseeks bookmarks; also bookmarks 2024-08-12 15:46:13 +02:00
2ab3f7ba7f update deps: heroicons 2024-08-12 11:55:58 +02:00
8383d00335 Merge branch 'feature/meeseeks-parser' into develop 2024-08-12 11:51:18 +02:00
1c1d218c82 Filter out ignored posts when parsing threads. Fixes #2 2024-08-12 11:48:36 +02:00
4cc2ee8276 add plausible tracking 2024-06-11 01:22:46 +02:00
36d97eebec Merge branch 'feature/meeseeks-parser' into develop 2024-06-04 00:08:38 +02:00
4a25eae6c6 meeseeks in 2024-06-04 00:06:23 +02:00
757c1fe063 start rewrite 2024-06-03 19:33:10 +02:00
10 changed files with 448 additions and 133 deletions

217
.credo.exs Normal file
View File

@ -0,0 +1,217 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: [
"lib/",
"src/",
"test/",
"web/",
"apps/*/lib/",
"apps/*/src/",
"apps/*/test/",
"apps/*/web/"
],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: true,
#
# To modify the timeout for parsing files, change this value:
#
parse_timeout: 5000,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: %{
enabled: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.TagFIXME, []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.Dbg, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.UnsafeExec, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.WrongTestFileExtension, []}
],
disabled: [
#
# Checks scheduled for next check update (opt-in for now)
{Credo.Check.Refactor.UtcNowTruncate, []},
#
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
# and be sure to use `mix credo --strict` to see low priority checks)
#
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
{Credo.Check.Consistency.UnusedVariableNames, []},
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []},
{Credo.Check.Readability.OneArityFunctionInPipe, []},
{Credo.Check.Readability.OnePipePerLine, []},
{Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PassAsyncInTestCases, []},
{Credo.Check.Refactor.PipeChainStart, []},
{Credo.Check.Refactor.RejectFilter, []},
{Credo.Check.Refactor.VariableRebinding, []},
{Credo.Check.Warning.LazyLogging, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []},
#
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
}
]
}

View File

@ -1,8 +1,7 @@
defmodule SomethingErlang.AwfulApi do
require Logger
alias SomethingErlang.AwfulApi.Thread
alias SomethingErlang.AwfulApi.Bookmarks
alias SomethingErlang.AwfulApi.{Client, Thread, Bookmarks}
@doc """
Returns a list of all posts on page of a thread.
@ -16,10 +15,12 @@ defmodule SomethingErlang.AwfulApi do
12
"""
def parsed_thread(id, page, user) do
Thread.compile(id, page, user)
Client.thread_doc(id, page, user)
|> Thread.compile()
end
def bookmarks(user) do
Bookmarks.compile(1, user)
def bookmarks(page, user) do
Client.bookmarks_doc(page, user)
|> Bookmarks.compile()
end
end

View File

@ -1,59 +1,84 @@
defmodule SomethingErlang.AwfulApi.Bookmarks do
import Meeseeks.CSS
require Logger
alias SomethingErlang.AwfulApi.Client
def compile(html) do
for thread <- Meeseeks.all(html, css("tr.thread")) do
thread_id =
Meeseeks.attr(thread, "id")
|> String.split("thread")
|> List.last()
|> String.to_integer()
def compile(page, user) do
doc = Client.bookmarks_doc(page, user)
html = Floki.parse_document!(doc)
for thread <- Floki.find(html, "tr.thread") do
parse(thread)
|> Map.put(:id, thread_id)
end
end
def parse(thread) do
%{
title: Floki.find(thread, "td.title") |> inner_html() |> Floki.raw_html(),
icon: Floki.find(thread, "td.icon") |> inner_html() |> Floki.raw_html(),
author: Floki.find(thread, "td.author") |> inner_html() |> Floki.text(),
replies: Floki.find(thread, "td.replies") |> inner_html() |> Floki.text(),
views: Floki.find(thread, "td.views") |> inner_html() |> Floki.text(),
rating: Floki.find(thread, "td.rating") |> inner_html() |> Floki.raw_html(),
lastpost: Floki.find(thread, "td.lastpost") |> inner_html() |> Floki.raw_html()
}
for {"td", [{"class", class} | _attrs], children} <- Floki.find(thread, "td"),
String.starts_with?(class, "star") == false,
for col <- Meeseeks.all(thread, css("td:not(.star)")),
class = Meeseeks.attr(col, "class") |> String.split() |> List.first(),
into: %{} do
case class do
<<"title", _rest::binary>> ->
{:title, children |> Floki.raw_html()}
<<"icon", _rest::binary>> ->
{:icon, children |> Floki.raw_html()}
<<"author", _rest::binary>> ->
{:author, children |> Floki.text()}
<<"replies", _rest::binary>> ->
{:replies, children |> Floki.text() |> String.to_integer()}
<<"views", _rest::binary>> ->
{:views, children |> Floki.text() |> String.to_integer()}
<<"rating", _rest::binary>> ->
{:rating, children |> Floki.raw_html()}
<<"lastpost", _rest::binary>> ->
{:lastpost, children |> Floki.raw_html()}
end
{String.to_atom(class), thread_data(class, col)}
end
end
defp inner_html(node) do
node
|> List.first()
|> Floki.children()
defp thread_data("icon", td) do
img = Meeseeks.one(td, css("img"))
%{
icon: Meeseeks.attr(img, "src"),
title: Meeseeks.attr(img, "alt")
}
end
defp thread_data("title", td) do
last_seen = Meeseeks.one(td, css(".lastseen .count"))
info = Meeseeks.one(td, css(".info"))
%{
new_posts:
case Meeseeks.text(last_seen) do
nil -> 0
n -> String.to_integer(n)
end,
thread_title: Meeseeks.text(Meeseeks.one(info, css(".thread_title")))
}
end
defp thread_data("author", td) do
Meeseeks.text(td)
end
defp thread_data("replies", td) do
Meeseeks.text(td)
|> String.to_integer()
end
defp thread_data("views", td) do
Meeseeks.text(td)
|> String.to_integer()
end
defp thread_data("rating", td) do
img = Meeseeks.one(td, css("img"))
%{
icon: Meeseeks.attr(img, "src"),
title: Meeseeks.attr(img, "title")
}
end
defp thread_data("lastpost", td) do
date = Meeseeks.one(td, css(".date"))
author = Meeseeks.one(td, css(".author"))
%{
date: Meeseeks.text(date),
author: Meeseeks.text(author)
}
end
defp thread_data(class, _td), do: Logger.error("#{inspect(class)} not found")
end

View File

@ -1,39 +1,60 @@
defmodule SomethingErlang.AwfulApi.Thread do
import Meeseeks.CSS
require Logger
alias SomethingErlang.AwfulApi.Client
def compile(html) do
title =
Meeseeks.one(html, css("title"))
|> Meeseeks.text()
|> String.replace(" - The Something Awful Forums", "")
def compile(id, page, user) do
doc = Client.thread_doc(id, page, user)
html = Floki.parse_document!(doc)
thread = Floki.find(html, "#thread") |> Floki.filter_out("table.post.ignored")
thread =
Meeseeks.one(html, css("#thread"))
title = Floki.find(html, "title") |> Floki.text()
title = title |> String.replace(" - The Something Awful Forums", "")
thread_id =
Meeseeks.attr(thread, "class")
|> String.split(":")
|> List.last()
|> String.to_integer()
page =
Meeseeks.one(html, css("#content .pages.top option[selected]"))
|> Meeseeks.text()
|> case do
"" -> 1
s -> String.to_integer(s)
end
page_count =
case Floki.find(html, "#content .pages.top option:last-of-type") |> Floki.text() do
Meeseeks.one(html, css("#content .pages.top option:last-of-type"))
|> Meeseeks.text()
|> case do
"" -> 1
s -> String.to_integer(s)
end
posts =
for post <- Floki.find(thread, "table.post") do
for post <- Meeseeks.all(thread, css("table.post:not(.ignored)")) do
%{
userinfo: post |> userinfo(),
postdate: post |> postdate(),
postbody: post |> postbody()
userinfo: userinfo(post),
postdate: postdate(post),
postbody: postbody(post)
}
end
%{id: id, title: title, page: page, page_count: page_count, posts: posts}
%{id: thread_id, title: title, page: page, page_count: page_count, posts: posts}
end
defp userinfo(post) do
user = Floki.find(post, "dl.userinfo")
name = user |> Floki.find("dt") |> Floki.text()
regdate = user |> Floki.find("dd.registered") |> Floki.text()
title = user |> Floki.find("dd.title") |> List.first() |> Floki.children() |> Floki.raw_html()
user = Meeseeks.one(post, css("dl.userinfo"))
name = user |> Meeseeks.one(css("dt")) |> Meeseeks.text()
regdate = user |> Meeseeks.one(css("dd.registered")) |> Meeseeks.text()
title =
user
|> Meeseeks.one(css("dd.title > *"))
|> Meeseeks.html()
%{
name: name,
@ -43,12 +64,15 @@ defmodule SomethingErlang.AwfulApi.Thread do
end
defp postdate(post) do
date = Floki.find(post, "td.postdate") |> Floki.find("td.postdate") |> Floki.text()
date =
post
|> Meeseeks.one(css("td.postdate"))
|> Meeseeks.text()
[month_text, day, year, hours, minutes] =
date
|> String.split(~r{[\s,:]}, trim: true)
|> Enum.drop(1)
|> Enum.drop(2)
month =
1 +
@ -68,34 +92,35 @@ defmodule SomethingErlang.AwfulApi.Thread do
end
defp postbody(post) do
body =
Floki.find(post, "td.postbody")
|> List.first()
|> Floki.filter_out(:comment)
{_, _, body} =
post
|> Meeseeks.one(css("td.postbody"))
|> Meeseeks.tree()
Floki.traverse_and_update(body, fn
{"img", attrs, []} -> transform(:img, attrs)
{"a", attrs, children} -> transform(:a, attrs, children)
other -> other
end)
|> Floki.children()
|> Floki.raw_html()
body
|> Enum.map(&transform/1)
|> Enum.reject(fn x -> x == "" end)
|> then(&{"div", [], &1})
|> Meeseeks.parse(:tuple_tree)
|> Meeseeks.html()
end
defp transform(elem, attr, children \\ [])
defp transform(:img, attrs, _children) do
defp transform({"img", attrs, _children}) do
{"class", class} = List.keyfind(attrs, "class", 0, {"class", ""})
if class == "sa-smilie" do
{"img", attrs, []}
else
t_attrs = List.keyreplace(attrs, "class", 0, {"class", "img-responsive"})
t_attrs =
attrs
|> fix_attachment_links()
|> List.keyreplace("class", 0, {"class", "img-responsive"})
{"img", [{"loading", "lazy"} | t_attrs], []}
end
end
defp transform(:a, attrs, children) do
defp transform({"a", attrs, children}) do
{"href", href} = List.keyfind(attrs, "href", 0, {"href", ""})
cond do
@ -124,6 +149,12 @@ defmodule SomethingErlang.AwfulApi.Thread do
end
end
defp transform({:comment, _}), do: ""
defp transform({tag, attrs, children}), do: {tag, attrs, children}
defp transform(text) when is_binary(text),
do: String.trim(text)
defp transform_link(:mp4, href),
do:
{"div", [{"class", "responsive-embed"}],
@ -167,4 +198,15 @@ defmodule SomethingErlang.AwfulApi.Thread do
{"src", src}
], []}
]}
defp fix_attachment_links(attrs) do
{"src", src} = List.keyfind(attrs, "src", 0)
if String.starts_with?(src, "attachment.php") do
List.keyreplace(attrs, "src", 0, {"src", "https://forums.somethingawful.com/" <> src})
|> List.keydelete("alt", 0)
else
attrs
end
end
end

View File

@ -4,48 +4,30 @@ defmodule SomethingErlang.Grover do
alias SomethingErlang.AwfulApi
require Logger
def mount(user) do
grover =
DynamicSupervisor.start_child(
SomethingErlang.Supervisor.Grovers,
{__MODULE__, [self(), user]}
)
case grover do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
{:error, error} -> {:error, error}
end
end
def get_thread!(thread_id, page_number) do
GenServer.call(via(self()), {:show_thread, thread_id, page_number})
GenServer.call(__MODULE__, {:show_thread, thread_id, page_number})
end
def get_bookmarks!(page_number) do
GenServer.call(via(self()), {:show_bookmarks, page_number})
def get_bookmarks!(pid, page_number) do
GenServer.call(pid, {:show_bookmarks, page_number})
end
def start_link([lv_pid, user]) do
def start_link(user) do
GenServer.start_link(
__MODULE__,
[lv_pid, user],
name: via(lv_pid)
[user]
)
end
@impl true
def init([pid, user]) do
def init([user]) do
%{bbuserid: userid, bbpassword: userhash} = user
initial_state = %{
lv_pid: pid,
user: %{id: userid, hash: userhash}
}
Logger.debug("init #{userid} #{inspect(pid)}")
Process.monitor(pid)
{:ok, initial_state}
{:ok, initial_state} |> dbg()
end
@impl true
@ -55,22 +37,8 @@ defmodule SomethingErlang.Grover do
end
@impl true
def handle_call({:show_bookmarks, _page_number}, _from, state) do
bookmarks = AwfulApi.bookmarks(state.user)
{:reply, bookmarks, state}
def handle_call({:show_bookmarks, page_number}, _from, state) do
bookmarks = AwfulApi.bookmarks(page_number, state.user)
{:reply, bookmarks, state} |> dbg()
end
@impl true
def handle_info({:DOWN, _ref, :process, _object, reason}, state) do
Logger.debug("received :DOWN from: #{inspect(state.lv_pid)} reason: #{inspect(reason)}")
case reason do
{:shutdown, _} -> {:stop, :normal, state}
:killed -> {:stop, :normal, state}
_ -> {:noreply, state}
end
end
defp via(lv_pid),
do: {:via, Registry, {SomethingErlang.Registry.Grovers, lv_pid}}
end

View File

@ -0,0 +1,54 @@
defmodule SomethingErlangWeb.BookmarksLive do
use SomethingErlangWeb, :live_view
alias SomethingErlang.Grover
def render(assigns) do
assigns.bookmarks |> dbg()
~H"""
<div :if={bookmarks = @bookmarks.ok? && @bookmarks.result}>
<.bookmark
:for={thread <- bookmarks}
id={thread.id}
title={thread.title.thread_title}
post_count={thread.replies}
/>
</div>
"""
end
defp bookmark(assigns) do
assigns = assign(assigns, :pages, ceil(assigns.post_count / 40))
~H"""
<div>
<.link href={~p"/thread/#{@id}"}><%= @title %></.link>
<.link href={~p"/thread/#{@id}?page=#{@pages}"}><%= @pages %></.link>
</div>
"""
end
def mount(_params, session, socket) do
user =
socket.assigns.current_user
|> Map.put(:bbpassword, session["bbpassword"])
{:ok, pid} = Grover.start_link(user) |> dbg()
{:ok,
socket
|> assign(:page_title, "Bookmarks")
|> assign_async(:bookmarks, fn ->
{:ok, bookmarks} = get_bookmarks(pid)
{:ok, %{bookmarks: bookmarks}}
end)}
end
defp get_bookmarks(pid) do
case Grover.get_bookmarks!(pid, 1) do
bookmarks when is_list(bookmarks) -> {:ok, bookmarks}
_ -> {:error, "Failed getting bookmarks"}
end
end
end

View File

@ -69,10 +69,10 @@ defmodule SomethingErlangWeb.ThreadLive do
~H"""
<div class="navbar my-4 bg-base-200">
<div class="flex-1"></div>
<div class="pagination flex-none btn-group grid grid-cols-5">
<div class="pagination join">
<%= for btn <- buttons(@thread) do %>
<.link
class={["btn btn-sm btn-ghost", btn.special]}
class={["btn btn-sm btn-ghost join-item", btn.special]}
navigate={~p"/thread/#{@thread.id}?page=#{btn.page}"}
>
<.label_button label={btn.label} page={btn.page} />
@ -86,8 +86,8 @@ defmodule SomethingErlangWeb.ThreadLive do
defp label_button(%{label: label} = assigns) do
case label do
"«" -> ~H{<.icon name="hero-chevron-double-left-mini" /><%= @page %>}
"" -> ~H{<.icon name="hero-chevron-left-mini" /><%= @page %>}
"" -> ~H{<%= @page %><.icon name="hero-chevron-right-mini" />}
"" -> ~H{<.icon name="hero-chevron-left-mini" />}
"" -> ~H{<.icon name="hero-chevron-right-mini" />}
"»" -> ~H{<%= @page %><.icon name="hero-chevron-double-right-mini" />}
_ -> ~H{<%= @page %>}
end

View File

@ -28,6 +28,8 @@ defmodule SomethingErlangWeb.Router do
on_mount: [{SomethingErlangWeb.UserAuth, :ensure_authenticated}] do
live("/thread", ThreadLive)
live("/thread/:id", ThreadLive)
live("/bookmarks", BookmarksLive)
live("/bookmarks/:id", BookmarksLive)
end
end

View File

@ -39,8 +39,7 @@ defmodule SomethingErlang.MixProject do
{:phoenix_html, "~> 4.0"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.20.2"},
# {:floki, ">= 0.30.0", only: :test},
{:floki, ">= 0.30.0"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
@ -59,7 +58,9 @@ defmodule SomethingErlang.MixProject do
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.2"},
{:req, "~> 0.5.0"}
{:meeseeks, "~> 0.17.0"},
{:req, "~> 0.5.0"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
end

View File

@ -1,6 +1,8 @@
%{
"bandit": {:hex, :bandit, "1.5.2", "ed0a41c43a9e529c670d0fd48371db4027e7b80d43b1942893e17deb8bed0540", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "35ddbdce7e8a2a3c6b5093f7299d70832a43ed2f4a1852885a61d334cab1b4ad"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
@ -15,6 +17,8 @@
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"meeseeks": {:hex, :meeseeks, "0.17.0", "8a41ceccd2365476c2b779292e7649fb25f0a9735030905941f1244d2095c8a6", [:mix], [{:meeseeks_html5ever, "~> 0.14.3", [hex: :meeseeks_html5ever, repo: "hexpm", optional: false]}], "hexpm", "13efaf321a1517dea046cb48ff9baa9dc0604d9afd82c57501bc01dc45a5e309"},
"meeseeks_html5ever": {:hex, :meeseeks_html5ever, "0.14.3", "7827c6ce393d9f99dd0220c356fd66ee7101718037ec6f7f18d4bcba84ef1798", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "6b69573b97120fcc6e97045178ad085fd3ee10a5b49c1e9ebb8a28bd4a9c538b"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
@ -31,6 +35,7 @@
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"},
"req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.6.3", "f838d94bc35e1844973ee7266127b156fdc962e9e8b7ff666c8fb4fed7964d23", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e18ecca3669a7454b3a2be75ae6c3ef01d550bc9a8cf5fbddcfff843b881d7c6"},
"swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},