From f6b262ea668bfaef48be40efb809e791258e2417 Mon Sep 17 00:00:00 2001 From: Logan Hunt Date: Thu, 21 Apr 2022 17:23:17 -0600 Subject: [PATCH] Updates to frontend and fix a bug where first socket assignment failed --- .gitignore | 3 +- assets/css/app.css | 119 ++---------------- assets/js/app.js | 30 ++--- assets/js/chat.js | 119 ++++++++++++++++-- lib/aggiedit_web/channels/post_channel.ex | 5 +- .../live/post_live/form_component.ex | 4 +- .../live/post_live/show.html.heex | 70 ++--------- .../templates/layout/root.html.heex | 1 + priv/static/cache_manifest.json | 6 + priv/static/favicon.ico | Bin 0 -> 15406 bytes 10 files changed, 161 insertions(+), 196 deletions(-) create mode 100644 priv/static/cache_manifest.json create mode 100644 priv/static/favicon.ico 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; } +.circle { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; /* or inline-flex */ + align-items: center; + justify-content: center; +} -/* Chat css from: https://www.bootdey.com/snippets/view/card-chat-list#html */ -.chat-container { - height: 300px; +.chat { + max-height: 50vh; overflow-y: scroll; } - -.chat-list { - padding: 0; - font-size: .8rem; - padding-bottom: 12px; -} - -.chat-list li { - margin-bottom: 10px; - overflow: auto; - color: #ffffff; -} - -.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; -} - -.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}`) - channel.join() - .receive("ok", resp => { console.log("Joined successfully: ", resp) }) - .receive("error", resp => { console.log("Unable to join: ", resp) }) - return channel; - }, -} +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]; -export default RoomChat; \ No newline at end of file +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); }); + 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 = ` +
+
+
${user.charAt(0)}
+
+
+
+
+ ${user} + ${new Date(inserted_at).toLocaleString()} +
+
+ ${body} +
+
+
+
+ `; + 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 = ` +
+ join: ${user} +
+ `; + 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; 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 @@ -
@@ -6,7 +5,7 @@
<%= if Ecto.assoc_loaded?(@post.upload) && !is_nil(@post.upload) do %> - + <% end %>
@@ -16,21 +15,18 @@ <%= live_patch "Edit", to: Routes.post_show_path(@socket, :edit, @room, @post), class: "button" %> | <% end %> <%= live_redirect "Back", to: Routes.post_index_path(@socket, :index, @room) %> -
-
- -
-
-
-
-
Chat
-
-
    -
-
-
-
+
+
+
+
+
+
+ + +
+ +
@@ -47,45 +43,3 @@ /> <% end %> - - 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" %> + 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 0000000000000000000000000000000000000000..de5936eb6f4ac03038cfa0fc04d2acc667898851 GIT binary patch literal 15406 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 Hu?GGF_Zr_S literal 0 HcmV?d00001