summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLogan Hunt <loganhunt@simponic.xyz>2022-04-21 17:23:17 -0600
committerLogan Hunt <loganhunt@simponic.xyz>2022-04-21 17:23:17 -0600
commitf6b262ea668bfaef48be40efb809e791258e2417 (patch)
tree260cc539adf551576f28c0af1b1dcdb456958c00
parentd9943b201d98a2396d62b9a190659e2e776da019 (diff)
downloadaggiedit-f6b262ea668bfaef48be40efb809e791258e2417.tar.gz
aggiedit-f6b262ea668bfaef48be40efb809e791258e2417.zip
Updates to frontend and fix a bug where first socket assignment failed
-rw-r--r--.gitignore3
-rw-r--r--assets/css/app.css119
-rw-r--r--assets/js/app.js30
-rw-r--r--assets/js/chat.js115
-rw-r--r--lib/aggiedit_web/channels/post_channel.ex5
-rw-r--r--lib/aggiedit_web/live/post_live/form_component.ex4
-rw-r--r--lib/aggiedit_web/live/post_live/show.html.heex70
-rw-r--r--lib/aggiedit_web/templates/layout/root.html.heex1
-rw-r--r--priv/static/cache_manifest.json6
-rw-r--r--priv/static/favicon.icobin0 -> 15406 bytes
10 files changed, 159 insertions, 194 deletions
diff --git a/.gitignore b/.gitignore
index a5ac23f..22cdcfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,6 @@
erl_crash.dump
*.ez
/assets/node_modules/
-/priv/static/
+/priv/static/uploads
+/priv/static/assets
/deps
diff --git a/assets/css/app.css b/assets/css/app.css
index 2cc38b4..0bab67f 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -126,115 +126,16 @@
white-space: pre-line;
}
-
-/* Chat css from: https://www.bootdey.com/snippets/view/card-chat-list#html */
-.chat-container {
- height: 300px;
- overflow-y: scroll;
-}
-
-.chat-list {
- padding: 0;
- font-size: .8rem;
- padding-bottom: 12px;
-}
-
-.chat-list li {
- margin-bottom: 10px;
- overflow: auto;
- color: #ffffff;
+.circle {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex; /* or inline-flex */
+ align-items: center;
+ justify-content: center;
}
-.chat-list .chat-img {
- float: left;
- width: 48px;
-}
-
-.chat-list .chat-img img {
- -webkit-border-radius: 50px;
- -moz-border-radius: 50px;
- border-radius: 50px;
- width: 100%;
-}
-
-.chat-list .chat-message {
- -webkit-border-radius: 50px;
- -moz-border-radius: 50px;
- border-radius: 50px;
- background: #5a99ee;
- display: inline-block;
- padding: 10px 20px;
- position: relative;
-}
-
-.chat-list .chat-message:before {
- content: "";
- position: absolute;
- top: 15px;
- width: 0;
- height: 0;
-}
-
-.chat-list .chat-message h5 {
- margin: 0 0 5px 0;
- font-weight: 600;
- line-height: 100%;
- font-size: .9rem;
-}
-
-.chat-list .chat-message p {
- line-height: 18px;
- margin: 0;
- padding: 0;
-}
-
-.chat-list .chat-body {
- margin-left: 20px;
- float: left;
- width: 70%;
-}
-
-.chat-list .in .chat-message:before {
- left: -12px;
- border-bottom: 20px solid transparent;
- border-right: 20px solid #5a99ee;
-}
-
-.chat-list .out .chat-img {
- float: right;
-}
-
-.chat-list .out .chat-body {
- float: right;
- margin-right: 20px;
- text-align: right;
-}
-
-.chat-list .out .chat-message {
- background: #fc6d4c;
-}
-
-.chat-list .out .chat-message:before {
- right: -12px;
- border-bottom: 20px solid transparent;
- border-left: 20px solid #fc6d4c;
-}
-
-.card .card-header:first-child {
- -webkit-border-radius: 0.3rem 0.3rem 0 0;
- -moz-border-radius: 0.3rem 0.3rem 0 0;
- border-radius: 0.3rem 0.3rem 0 0;
-}
-.card .card-header {
- background: #17202b;
- border: 0;
- font-size: 1rem;
- padding: .65rem 1rem;
- position: relative;
- font-weight: 600;
- color: #ffffff;
+.chat {
+ max-height: 50vh;
+ overflow-y: scroll;
}
-
-.content{
- margin-top:40px;
-} \ No newline at end of file
diff --git a/assets/js/app.js b/assets/js/app.js
index 94780d0..92b68d1 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1,6 +1,6 @@
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
-import "../css/app.css"
+import "../css/app.css";
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
@@ -20,32 +20,32 @@ import "../css/app.css"
//
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
-import "phoenix_html"
+import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration.
-import {Socket} from "phoenix"
-import {LiveSocket} from "phoenix_live_view"
-import topbar from "../vendor/topbar"
+import {Socket} from "phoenix";
+import {LiveSocket} from "phoenix_live_view";
+import topbar from "../vendor/topbar";
-let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
-let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
+let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});
// Show progress bar on live navigation and form submits
-topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
-window.addEventListener("phx:page-loading-start", info => topbar.show())
-window.addEventListener("phx:page-loading-stop", info => topbar.hide())
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"});
+window.addEventListener("phx:page-loading-start", info => topbar.show());
+window.addEventListener("phx:page-loading-stop", info => topbar.hide());
// connect if there are any LiveViews on the page
-liveSocket.connect()
+liveSocket.connect();
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
-window.liveSocket = liveSocket
+window.liveSocket = liveSocket;
// Hack to remove alerts on click
-Array.from(window.document.getElementsByClassName('alert')).forEach((x) => x.addEventListener('click', () => x.style.display = "none"))
+Array.from(window.document.getElementsByClassName('alert')).forEach((x) => x.addEventListener('click', () => x.style.display = "none"));
-import RoomChat from "./chat"
+window.userSocket = new Socket("/socket", {params: {_csrf_token: csrfToken}});
+import RoomChat from "./chat";
window.RoomChat = RoomChat;
-window.userSocket = new Socket("/socket", {params: {_csrf_token: csrfToken}}) \ No newline at end of file
diff --git a/assets/js/chat.js b/assets/js/chat.js
index 4183531..aa7f05d 100644
--- a/assets/js/chat.js
+++ b/assets/js/chat.js
@@ -1,11 +1,110 @@
-let RoomChat = {
- connect(socket, postId) {
- let channel = socket.channel(`post:${postId}`)
+const gruvboxColors = [
+ "#b8bb26",
+ "#fabd2f",
+ "#83a598",
+ "#d3869b",
+ "#8ec07c",
+ "#458588",
+ "#cc241d",
+ "#d65d0e",
+ "#bdae93",
+];
+const generateGruvboxFromString = (string) =>
+ gruvboxColors[Array.from(string).map((x) => x.charCodeAt(0)).reduce((a, x) => a+x, 0) % gruvboxColors.length];
+
+const RoomChat = (() => {
+ let channel;
+ const connect = (socket, postId) => {
+ channel = socket.channel(`post:${postId}`);
channel.join()
- .receive("ok", resp => { console.log("Joined successfully: ", resp) })
- .receive("error", resp => { console.log("Unable to join: ", resp) })
+ .receive("ok", resp => { console.log("Joined successfully: ", resp); })
+ .receive("error", resp => { console.log("Unable to join: ", resp); });
return channel;
- },
-}
+ };
+
+ const scrollToBottom = (element) => {
+ element.scrollTop = element.scrollHeight;
+ };
+
+ const appendComment = ({user, body, id, user_id, inserted_at}, element) => {
+ const messageElement = document.createElement("div");
+ messageElement.innerHTML = `
+ <div class="d-flex flex-row card border rounded m-2 align-items-center">
+ <div class="m-2">
+ <div class="circle" style="background:${generateGruvboxFromString(user)}">${user.charAt(0)}</div>
+ </div>
+ <div class="m-2">
+ <div class="comment">
+ <div class="comment-header">
+ <span class="comment-username">${user}</span>
+ <span class="text-muted">${new Date(inserted_at).toLocaleString()}</span>
+ </div>
+ <div class="comment-body">
+ ${body}
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+ element.appendChild(messageElement);
+ scrollToBottom(element);
+ };
+
+ const leaveChannel = () => {
+ if (channel) {
+ channel.leave();
+ console.log(channel);
+ }
+ };
+
+ const main = (post_id) => {
+ leaveChannel();
+ const chatWindow = document.getElementById("chat");
+ window.userSocket.connect();
+ channel = connect(window.userSocket, post_id);
+
+ channel.on('shout', (comment) => {
+ appendComment(comment, chatWindow);
+ });
+
+ channel.on('initial-comments', ({comments}) => {
+ comments.forEach((comment) => {
+ appendComment(comment, chatWindow);
+ });
+ scrollToBottom(chatWindow);
+ });
+
+ channel.on('join', ({ user }) => {
+ const joinElement = document.createElement("div");
+ joinElement.innerHTML = `
+ <div class="m-2 card border rounded p-2 text-muted">
+ join: ${user}
+ </div>
+ `;
+ chatWindow.appendChild(joinElement);
+ scrollToBottom(chatWindow);
+ });
+
+ channel.on('left', ({ user }) => {
+ console.log(user, "left");
+ });
+ };
+
+ const submitForm = (e) => {
+ e.preventDefault();
+ let message = e.target.elements.message.value;
+ if (message) {
+ channel.push("send", {body: message});
+ e.target.elements.message.value = "";
+ }
+ return false;
+ };
+
+ return { main, submitForm };
+})();
+
+window.addEventListener('load', () => {
+ window.addEventListener('phx:initial-post', (e) => RoomChat.main(e.detail.id));
+});
-export default RoomChat; \ No newline at end of file
+export default RoomChat;
diff --git a/lib/aggiedit_web/channels/post_channel.ex b/lib/aggiedit_web/channels/post_channel.ex
index ea79d76..2b1c9b5 100644
--- a/lib/aggiedit_web/channels/post_channel.ex
+++ b/lib/aggiedit_web/channels/post_channel.ex
@@ -24,13 +24,14 @@ defmodule AggieditWeb.PostChannel do
|> Enum.map(fn c -> Aggiedit.Post.Comment.serialize(c) end)
push(socket, "initial-comments", %{:comments => comments})
+ broadcast!(socket, "join", %{user: socket.assigns.current_user.username})
{:noreply, socket}
end
@impl true
- def handle_in("send", %{"body" => comment}=body, socket) do
+ def handle_in("send", %{"body" => comment}, socket) do
{:ok, comment} = Rooms.comment_post(socket.assigns.post, socket.assigns.current_user, comment)
broadcast!(socket, "shout", Aggiedit.Post.Comment.serialize(comment))
{:reply, :ok, socket}
end
-end \ No newline at end of file
+end
diff --git a/lib/aggiedit_web/live/post_live/form_component.ex b/lib/aggiedit_web/live/post_live/form_component.ex
index 8714277..43e6b9b 100644
--- a/lib/aggiedit_web/live/post_live/form_component.ex
+++ b/lib/aggiedit_web/live/post_live/form_component.ex
@@ -39,7 +39,9 @@ defmodule AggieditWeb.PostLive.FormComponent do
filename = "#{upload.uuid}.#{extension}"
dest = Path.join("priv/static/uploads", filename)
- File.cp!(data.path, dest)
+ with :ok <- File.mkdir_p(Path.dirname(dest)) do
+ File.cp!(data.path, dest)
+ end
{:ok, upload} = Uploads.create_upload(%{
file: filename,
diff --git a/lib/aggiedit_web/live/post_live/show.html.heex b/lib/aggiedit_web/live/post_live/show.html.heex
index b89999b..f0d1f41 100644
--- a/lib/aggiedit_web/live/post_live/show.html.heex
+++ b/lib/aggiedit_web/live/post_live/show.html.heex
@@ -1,4 +1,3 @@
-
<div class="d-flex justify-content-center">
<div class="container">
<div>
@@ -6,7 +5,7 @@
</div>
<div>
<%= if Ecto.assoc_loaded?(@post.upload) && !is_nil(@post.upload) do %>
- <img src={Routes.static_path(@socket, "/uploads/#{@post.upload.file}")} class="img-fluid"/>
+ <img src={Routes.static_path(@socket, "/uploads/#{@post.upload.file}")} class="img-fluid" style="max-height: 40vh"/>
<% end %>
</div>
<div class="post-body">
@@ -16,21 +15,18 @@
<span><%= live_patch "Edit", to: Routes.post_show_path(@socket, :edit, @room, @post), class: "button" %></span> |
<% end %>
<span><%= live_redirect "Back", to: Routes.post_index_path(@socket, :index, @room) %></span>
- </div>
-</div>
-<!-- chat container from https://www.bootdey.com/snippets/view/card-chat-list#html -->
-<div class="container content mt-2">
- <div class="row">
- <div class="col-xl-6 col-lg-6 col-md-6 col-sm-12 col-12">
- <div class="card">
- <div class="card-header">Chat</div>
- <div class="card-body chat-container">
- <ul class="chat-list" id="chat">
- </ul>
- </div>
- </div>
- </div>
+ <div class="border rounded p-2 m-2">
+ <div class="chat" id="chat">
+ </div>
+ </div>
+ <form class="border rounded p-2 m-2" onsubmit="return RoomChat.submitForm(event)">
+ <div class="form-group m-2">
+ <label for="message">Message</label>
+ <input type="text" class="form-control" id="message" name="message" placeholder="Message">
+ </div>
+ <button type="submit" class="m-2 btn btn-primary">Submit</button>
+ </form>
</div>
</div>
@@ -47,45 +43,3 @@
/>
</.modal>
<% end %>
-
-<script>
- const scrollToBottom = (element) => {
- element.scrollIntoView({ behavior: 'smooth', block: 'end' });
- };
-
- const appendComment = ({user, body, id, user_id, inserted_at}, element) => {
- const messageElement = document.createElement("div");
- messageElement.innerHTML = `
- <li class="${user_id == <%= @current_user.id %> ? 'out' : 'in'}" id=${id}>
- <div class="chat-body">
- <div class="chat-message">
- <h5>${user}</h5>
- <p>${body}</p>
- </div>
- </div>
- </li>
- `;
- element.appendChild(messageElement);
- scrollToBottom(element);
- };
-
-
- let channel;
- window.addEventListener('phx:initial-post', (e) => {
- const chatWindow = document.getElementById("chat");
- window.userSocket.connect();
- channel = window.RoomChat.connect(window.userSocket, e.detail.id);
-
- channel.on('shout', (comment) => {
- appendComment(comment, chatWindow);
- });
-
- channel.on('initial-comments', ({comments}) => {
- comments.forEach((comment) => {
- appendComment(comment, chatWindow);
- });
- scrollToBottom(chatWindow);
- });
- channel.push("send", {body: "Hello!"});
- });
-</script>
diff --git a/lib/aggiedit_web/templates/layout/root.html.heex b/lib/aggiedit_web/templates/layout/root.html.heex
index 14c7605..ec7ff0d 100644
--- a/lib/aggiedit_web/templates/layout/root.html.heex
+++ b/lib/aggiedit_web/templates/layout/root.html.heex
@@ -8,6 +8,7 @@
<%= live_title_tag assigns[:page_title] || "Aggiedit" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
<script phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
+ <link rel="icon" type="image/x-icon" href={Routes.static_path(@conn, "/favicon.ico")}/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
diff --git a/priv/static/cache_manifest.json b/priv/static/cache_manifest.json
new file mode 100644
index 0000000..5bcb5e6
--- /dev/null
+++ b/priv/static/cache_manifest.json
@@ -0,0 +1,6 @@
+{
+ "!comment!":"This is file was auto-generated by `mix phx.digest`. Remove it and all generated artefacts with `mix phx.digest.clean --all`",
+ "version":1,
+ "latest":{"assets/app.css":"assets/app-72a7491b6b9757035f95db8c5c5cf678.css","assets/app.js":"assets/app-918e6c6babd85e4c7c00c196f6812eb8.js","uploads/2131d96a-ca70-4b85-adad-4d5e79d75f14.gif":"uploads/2131d96a-ca70-4b85-adad-4d5e79d75f14-8764b2c550af1698c198d905e2effaee.gif","uploads/2b248619-0108-4491-a274-fc9612734eec.png":"uploads/2b248619-0108-4491-a274-fc9612734eec-652a26601d77a8a0aea9ac93d9c228a8.png","uploads/560289fb-8034-473d-bdac-d2809ea12657.jpg":"uploads/560289fb-8034-473d-bdac-d2809ea12657-a869361e51bac0921ad981d69de4dab1.jpg","uploads/5dd96713-6569-4f91-ba2f-80704eebec2c.png":"uploads/5dd96713-6569-4f91-ba2f-80704eebec2c-b93ab29b0ddb0df45cc1b07383cdc055.png","uploads/611a22bd-127e-4001-95be-098bfe2f7727.png":"uploads/611a22bd-127e-4001-95be-098bfe2f7727-0d4b8caad63107d19ad5d3ecf808b8d4.png","uploads/720c6fa8-9aaf-4f57-9140-d8385e95329a.png":"uploads/720c6fa8-9aaf-4f57-9140-d8385e95329a-0eeba950f7798b4e9dc419dc49b74e0e.png","uploads/8422a784-eca2-4b8f-b11c-0e1ea43a3f5d.jpg":"uploads/8422a784-eca2-4b8f-b11c-0e1ea43a3f5d-52d2d2e93777c59224152c32429e621f.jpg","uploads/854ed9b7-6402-4b4c-ac22-6566656ade47.png":"uploads/854ed9b7-6402-4b4c-ac22-6566656ade47-4bd02909ddb99d836bd592745b742a97.png","uploads/8708f155-6cb9-4428-9be0-7ef18009b35a.gif":"uploads/8708f155-6cb9-4428-9be0-7ef18009b35a-8764b2c550af1698c198d905e2effaee.gif","uploads/9a2521a7-23ce-4e8d-b7e6-fd10612f61d9.png":"uploads/9a2521a7-23ce-4e8d-b7e6-fd10612f61d9-b0d9ad3620e10f3d8db27adb0aad2be2.png","uploads/9e9e0be1-bb9c-4f90-8775-caa965a8115f.png":"uploads/9e9e0be1-bb9c-4f90-8775-caa965a8115f-eff3f6f9e7ae9e5b6ade8cde27b633e6.png","uploads/a8d51a71-0237-4ccd-a0e5-ef9708c1707c.png":"uploads/a8d51a71-0237-4ccd-a0e5-ef9708c1707c-eff3f6f9e7ae9e5b6ade8cde27b633e6.png","uploads/ac5400a8-fcfe-49c1-8040-0cce4b6152e4.png":"uploads/ac5400a8-fcfe-49c1-8040-0cce4b6152e4-4bd02909ddb99d836bd592745b742a97.png","uploads/ae175b1c-733b-42e8-bdf5-9a57d4310bbf.png":"uploads/ae175b1c-733b-42e8-bdf5-9a57d4310bbf-06dee130711273259125186d935ab20b.png","uploads/b7857b18-a63b-4fb5-a919-09176a29ce3c.png":"uploads/b7857b18-a63b-4fb5-a919-09176a29ce3c-f5ae02969afac26992ba094b19f3cda0.png","uploads/b833d274-abed-42ee-9a2b-fd1278d110c5.png":"uploads/b833d274-abed-42ee-9a2b-fd1278d110c5-06dee130711273259125186d935ab20b.png","uploads/f2231a4d-51e2-4f87-ab22-75fbea8e8cb4.jpg":"uploads/f2231a4d-51e2-4f87-ab22-75fbea8e8cb4-0e15b488641e85bcc60b7519f3ed9028.jpg","uploads/f7b51a4b-cd95-45fb-9cd6-6feeff8a631c.png":"uploads/f7b51a4b-cd95-45fb-9cd6-6feeff8a631c-00fb2892f1d550ec1aa5343b8e27139a.png"},
+ "digests":{"assets/app-72a7491b6b9757035f95db8c5c5cf678.css":{"digest":"72a7491b6b9757035f95db8c5c5cf678","logical_path":"assets/app.css","mtime":63817800856,"sha512":"932tLzggftQMEzxYiZoZ+UQCDNF+/7w9EkZmV62jDElG7O9HKalplVBNvMzH+U3vEzKRyHlE+PeS3AHJaoGTnw==","size":6693},"assets/app-918e6c6babd85e4c7c00c196f6812eb8.js":{"digest":"918e6c6babd85e4c7c00c196f6812eb8","logical_path":"assets/app.js","mtime":63817800856,"sha512":"nX5OskADombMkmd0MVlVH6nEaln/e5t37AaSMamliwiumSegt3dx+ULTrs7wBALPqUgVGEw8EU67mRaaWoPb5w==","size":605195},"favicon-151fdae605fe8991df76f1d88259ea9f.ico":{"digest":"151fdae605fe8991df76f1d88259ea9f","logical_path":"favicon.ico","mtime":63817800808,"sha512":"v4WRz7SbcUzAGMAuW7D5Uz5+zTfdMSpfiMnvvm7wGm7UbIBn/LVFjoRURlr3UKnBpb7/VIVOoNw47AA3B27u2Q==","size":15406},"uploads/2131d96a-ca70-4b85-adad-4d5e79d75f14-8764b2c550af1698c198d905e2effaee.gif":{"digest":"8764b2c550af1698c198d905e2effaee","logical_path":"uploads/2131d96a-ca70-4b85-adad-4d5e79d75f14.gif","mtime":63817800856,"sha512":"zOUMadyCx17IrHM06XOeu3T8m9yBwzUqtubLwyoSL9EpebK7dBEeqhekRedrYQwSSF+KpgEiDDobC4XRJyXeqw==","size":19586},"uploads/2b248619-0108-4491-a274-fc9612734eec-652a26601d77a8a0aea9ac93d9c228a8.png":{"digest":"652a26601d77a8a0aea9ac93d9c228a8","logical_path":"uploads/2b248619-0108-4491-a274-fc9612734eec.png","mtime":63817800856,"sha512":"6x0uKYwaVvoLvK/1wro1Srg+ffPCkyfuAvZ8XyrjeY54Eo7+OPRUhok6NF45nZg4+JvgctSYI6CuGpkQiBPyZg==","size":97695},"uploads/560289fb-8034-473d-bdac-d2809ea12657-a869361e51bac0921ad981d69de4dab1.jpg":{"digest":"a869361e51bac0921ad981d69de4dab1","logical_path":"uploads/560289fb-8034-473d-bdac-d2809ea12657.jpg","mtime":63817800856,"sha512":"QAslYQjiLWBBReFcLL9XZPUgNB5pzldaBc1wf9pXZiWpa8SrLOaZj4xEJhNlfTKX0z6dsBWtxFpy4Cx5D3ahEQ==","size":1120044},"uploads/5dd96713-6569-4f91-ba2f-80704eebec2c-b93ab29b0ddb0df45cc1b07383cdc055.png":{"digest":"b93ab29b0ddb0df45cc1b07383cdc055","logical_path":"uploads/5dd96713-6569-4f91-ba2f-80704eebec2c.png","mtime":63817800856,"sha512":"B6ePAJCTEJFCJONDstFkuVa7PnuQmD0m8NjJPNI9X82TIj8MoRVBbdk57cPIOWUBf94Wop9Q43a0SZX5qw9JlQ==","size":1315},"uploads/611a22bd-127e-4001-95be-098bfe2f7727-0d4b8caad63107d19ad5d3ecf808b8d4.png":{"digest":"0d4b8caad63107d19ad5d3ecf808b8d4","logical_path":"uploads/611a22bd-127e-4001-95be-098bfe2f7727.png","mtime":63817800856,"sha512":"p0WaRtw14olGn9XnyzfI1Sa+mZz5KmEIuqYebxYgVdSiGh/wPKjOHxkFtrzj9FIeloF1QxXAPjXn/PIiXbSekQ==","size":85625},"uploads/720c6fa8-9aaf-4f57-9140-d8385e95329a-0eeba950f7798b4e9dc419dc49b74e0e.png":{"digest":"0eeba950f7798b4e9dc419dc49b74e0e","logical_path":"uploads/720c6fa8-9aaf-4f57-9140-d8385e95329a.png","mtime":63817800856,"sha512":"SJBU3KfwHY+ndMqIittKvJraIEn0qtGZgCl6IuHWXbGSwuy8JoLvHOUN2Ao6oH/1222vqNSX+4RWnnglmdYQ8A==","size":81689},"uploads/8422a784-eca2-4b8f-b11c-0e1ea43a3f5d-52d2d2e93777c59224152c32429e621f.jpg":{"digest":"52d2d2e93777c59224152c32429e621f","logical_path":"uploads/8422a784-eca2-4b8f-b11c-0e1ea43a3f5d.jpg","mtime":63817800856,"sha512":"tT8K/w/yo1dsBMOP8HwEreiuMPC5HyxZU30ax5lt3dxYUw/hWYOAxliLb3KQh/KV9yoVJadH3HOuFQO9pjiFQQ==","size":1480496},"uploads/854ed9b7-6402-4b4c-ac22-6566656ade47-4bd02909ddb99d836bd592745b742a97.png":{"digest":"4bd02909ddb99d836bd592745b742a97","logical_path":"uploads/854ed9b7-6402-4b4c-ac22-6566656ade47.png","mtime":63817800856,"sha512":"Yj2OuPwN+AnUMbVeUIiIYB9YJww9e57nRQtlTwmOgWRncWtLc4VOn+4hMg2LhkwFdtCgG314MmjIvQgoLP6b5g==","size":69704},"uploads/8708f155-6cb9-4428-9be0-7ef18009b35a-8764b2c550af1698c198d905e2effaee.gif":{"digest":"8764b2c550af1698c198d905e2effaee","logical_path":"uploads/8708f155-6cb9-4428-9be0-7ef18009b35a.gif","mtime":63817800856,"sha512":"zOUMadyCx17IrHM06XOeu3T8m9yBwzUqtubLwyoSL9EpebK7dBEeqhekRedrYQwSSF+KpgEiDDobC4XRJyXeqw==","size":19586},"uploads/9a2521a7-23ce-4e8d-b7e6-fd10612f61d9-b0d9ad3620e10f3d8db27adb0aad2be2.png":{"digest":"b0d9ad3620e10f3d8db27adb0aad2be2","logical_path":"uploads/9a2521a7-23ce-4e8d-b7e6-fd10612f61d9.png","mtime":63817800856,"sha512":"wbYKe9aQwYqgz4qmLu5JOLC5itbugAuwp7sGDJb4N3nLWHEsU6lWyX1vVCc70+NGp53fupgvak07/fa0mA6kUg==","size":8145},"uploads/9e9e0be1-bb9c-4f90-8775-caa965a8115f-eff3f6f9e7ae9e5b6ade8cde27b633e6.png":{"digest":"eff3f6f9e7ae9e5b6ade8cde27b633e6","logical_path":"uploads/9e9e0be1-bb9c-4f90-8775-caa965a8115f.png","mtime":63817800856,"sha512":"Jp0RqtKjfVnz4GiInODX8hbJXb9TL9S2BI2/I8n73Ymacs0RIdDndUKVjszgbUwQNyZgvpVLJQYGLmeElGYD/Q==","size":54751},"uploads/a8d51a71-0237-4ccd-a0e5-ef9708c1707c-eff3f6f9e7ae9e5b6ade8cde27b633e6.png":{"digest":"eff3f6f9e7ae9e5b6ade8cde27b633e6","logical_path":"uploads/a8d51a71-0237-4ccd-a0e5-ef9708c1707c.png","mtime":63817800856,"sha512":"Jp0RqtKjfVnz4GiInODX8hbJXb9TL9S2BI2/I8n73Ymacs0RIdDndUKVjszgbUwQNyZgvpVLJQYGLmeElGYD/Q==","size":54751},"uploads/ac5400a8-fcfe-49c1-8040-0cce4b6152e4-4bd02909ddb99d836bd592745b742a97.png":{"digest":"4bd02909ddb99d836bd592745b742a97","logical_path":"uploads/ac5400a8-fcfe-49c1-8040-0cce4b6152e4.png","mtime":63817800856,"sha512":"Yj2OuPwN+AnUMbVeUIiIYB9YJww9e57nRQtlTwmOgWRncWtLc4VOn+4hMg2LhkwFdtCgG314MmjIvQgoLP6b5g==","size":69704},"uploads/ae175b1c-733b-42e8-bdf5-9a57d4310bbf-06dee130711273259125186d935ab20b.png":{"digest":"06dee130711273259125186d935ab20b","logical_path":"uploads/ae175b1c-733b-42e8-bdf5-9a57d4310bbf.png","mtime":63817800856,"sha512":"HqOiCr7nnH0zEwJtAZBtAF5MR4FahojN5qN/htglG68FgVHGZqB/6gVlaCLgluA7N+t4EDrmMwn6OVWZEH9HrQ==","size":17585},"uploads/b7857b18-a63b-4fb5-a919-09176a29ce3c-f5ae02969afac26992ba094b19f3cda0.png":{"digest":"f5ae02969afac26992ba094b19f3cda0","logical_path":"uploads/b7857b18-a63b-4fb5-a919-09176a29ce3c.png","mtime":63817800856,"sha512":"wx7SWUIs74wy0pn4L8vzp/gkZM95GHzTsmnrikEGk33O+0GxxTq0xt/h+CZ0dNtARlgcmYUsjTV+N5u+gAGFVQ==","size":39397},"uploads/b833d274-abed-42ee-9a2b-fd1278d110c5-06dee130711273259125186d935ab20b.png":{"digest":"06dee130711273259125186d935ab20b","logical_path":"uploads/b833d274-abed-42ee-9a2b-fd1278d110c5.png","mtime":63817800856,"sha512":"HqOiCr7nnH0zEwJtAZBtAF5MR4FahojN5qN/htglG68FgVHGZqB/6gVlaCLgluA7N+t4EDrmMwn6OVWZEH9HrQ==","size":17585},"uploads/f2231a4d-51e2-4f87-ab22-75fbea8e8cb4-0e15b488641e85bcc60b7519f3ed9028.jpg":{"digest":"0e15b488641e85bcc60b7519f3ed9028","logical_path":"uploads/f2231a4d-51e2-4f87-ab22-75fbea8e8cb4.jpg","mtime":63817800856,"sha512":"MPkFBTAIVwP6GTpi+Knvqs60wXujChpOoQwf6izfq3ihuEStt49rOSyH8mvXJ6g3KSiy1QNx6olSzQ6+vSxPXg==","size":85345},"uploads/f7b51a4b-cd95-45fb-9cd6-6feeff8a631c-00fb2892f1d550ec1aa5343b8e27139a.png":{"digest":"00fb2892f1d550ec1aa5343b8e27139a","logical_path":"uploads/f7b51a4b-cd95-45fb-9cd6-6feeff8a631c.png","mtime":63817800856,"sha512":"6GhicFiT5GqfijnFutKyGCwup2dC0De7cmLee5va7RUiCTDEoyW1qza/0PQIHvi3bRdXRZDy9nFkNLVtFHv1Ng==","size":39676}}
+}
diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico
new file mode 100644
index 0000000..de5936e
--- /dev/null
+++ b/priv/static/favicon.ico
Binary files differ