Skip to content

Commit

Permalink
Save brainstormings in local storage (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
JannikStreek authored Dec 18, 2024
1 parent 4821fb8 commit 21d1680
Show file tree
Hide file tree
Showing 30 changed files with 679 additions and 348 deletions.
11 changes: 11 additions & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
!*.heex
!*.ex
!*.exs
!*.json
!*.js
!*.jsx
!*.ts
!*.tsx
!*.scss
!*.css
!*.html
43 changes: 41 additions & 2 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Modal, Tooltip } from "bootstrap"
import Sortable from 'sortablejs';
import { setIdeaLabelBackgroundColor } from "./label"

import { setIdeaLabelBackgroundColor } from "./label";
// activate all tooltips:
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl));

const sortBrainstormingsByLastAccessedAt = (brainstormings, sliceMax = 10) => {
return Object.values(brainstormings).sort((a, b) => new Date(b.last_accessed_at) - new Date(a.last_accessed_at)).slice(0, sliceMax)
}

// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
Expand Down Expand Up @@ -131,6 +134,42 @@ Hooks.SetIdeaLabelBackgroundColor = {
}
};

Hooks.TransferLocalStorageBrainstormings = {
mounted() {
const recentBrainstormings = JSON.parse(localStorage.getItem('brainstormings') || '{}');
const lastSortedBrainstormings = sortBrainstormingsByLastAccessedAt(recentBrainstormings, 5)
this.pushEventTo(this.el, "brainstormings_from_local_storage", lastSortedBrainstormings)
}
}

Hooks.StoreRecentBrainstorming = {
mounted() {
const brainstormingId = this.el.dataset.id;
const recentBrainstormings = JSON.parse(localStorage.getItem('brainstormings') || '{}');

recentBrainstormings[brainstormingId] = {
id: brainstormingId,
admin_url_id: this.el.dataset.adminUrlId || recentBrainstormings?.brainstormingId?.admin_url_id,
name: this.el.dataset.name,
last_accessed_at: this.el.dataset.lastAccessedAt
}
localStorage.setItem('brainstormings', JSON.stringify(recentBrainstormings));
const lastSortedBrainstormings = sortBrainstormingsByLastAccessedAt(recentBrainstormings)
this.pushEventTo(this.el,"brainstormings_from_local_storage", lastSortedBrainstormings)
}
};

Hooks.RemoveMissingBrainstorming = {
mounted() {
const missingId = this.el.dataset.brainstormingId;
if (missingId) {
const recentBrainstormings = JSON.parse(localStorage.getItem('brainstormings') || '{}');
delete recentBrainstormings[missingId]
localStorage.setItem('brainstormings', JSON.stringify(recentBrainstormings));
}
}
};

// The brainstorming secret from the url ("#123") is added as well to the socket. The secret is not available on the server side by default.
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks, params: { _csrf_token: csrfToken, adminSecret: window.location.hash.substring(1) }
Expand Down
6 changes: 6 additions & 0 deletions assets/scss/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ a .bi {

/* LiveView specific classes for your customizations */

// similar to .phx-connected but also works when javascript is disabled
div[data-phx-session] {
height: 100%;
width: 100%;
}

.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
Expand Down
10 changes: 8 additions & 2 deletions lib/mindwendel/brainstormings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ defmodule Mindwendel.Brainstormings do
[%Brainstorming{}, ...]
"""
def list_brainstormings_for(user_id, limit \\ 3) do
def list_brainstormings_for(user_id, limit \\ 3)

def list_brainstormings_for(user_id, limit) when user_id != nil do
Repo.all(
from brainstorming in Brainstorming,
join: users in assoc(brainstorming, :users),
where: users.id == ^user_id,
order_by: [desc: brainstorming.inserted_at],
order_by: [desc: brainstorming.last_accessed_at],
limit: ^limit
)
end

def list_brainstormings_for(nil, _) do
[]
end

@doc """
Returns the list of brainstormings.
Expand Down
2 changes: 1 addition & 1 deletion lib/mindwendel/likes.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Mindwendel.Likes do
@moduledoc """
The Brainstormings context.
The Likes context.
"""

import Ecto.Query, warn: false
Expand Down
70 changes: 70 additions & 0 deletions lib/mindwendel/local_storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule Mindwendel.LocalStorage do
alias Mindwendel.Permissions

@moduledoc """
The LocalStorage context. It includes helpers to handle local storage data from clients.
"""

# This function is used to merge the brainstormings from local storage and the session.
# It returns a list of brainstormings sorted by the last_accessed_at field.
def brainstormings_from_local_storage_and_session(
brainstormings_from_local_storage,
brainstormings_from_session,
user
) do
(brainstormings_from_local_storage(brainstormings_from_local_storage) ++
brainstormings_from_session(brainstormings_from_session, user))
|> Enum.uniq_by(& &1["id"])
|> Enum.sort(&(&1["last_accessed_at"] > &2["last_accessed_at"]))
end

defp brainstormings_from_local_storage(brainstormings_stored)
when is_list(brainstormings_stored) do
brainstormings_stored
|> Enum.map(fn e ->
Map.put(e, "last_accessed_at", format_iso8601(e["last_accessed_at"]))
end)
|> Enum.filter(&valid_stored_brainstorming?/1)
end

defp brainstormings_from_local_storage(_) do
[]
end

defp brainstormings_from_session(brainstormings, user) when is_list(brainstormings) do
Enum.map(brainstormings, fn brainstorming ->
%{
"last_accessed_at" => brainstorming.last_accessed_at,
"name" => brainstorming.name,
"id" => brainstorming.id,
"admin_url_id" =>
if(Permissions.has_moderating_permission(brainstorming, user),
do: brainstorming.admin_url_id,
else: nil
)
}
end)
end

defp brainstormings_from_session(_, _) do
[]
end

defp valid_stored_brainstorming?(brainstorming) do
case Ecto.UUID.cast(brainstorming["id"]) do
{:ok, _} -> brainstorming["last_accessed_at"] && brainstorming["name"]
:error -> false
end
end

defp format_iso8601(iso8601) when iso8601 != nil do
case DateTime.from_iso8601(iso8601) do
{:ok, date_time, _} -> date_time
{:error, _} -> nil
end
end

defp format_iso8601(_) do
nil
end
end
12 changes: 12 additions & 0 deletions lib/mindwendel/permissions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Mindwendel.Permissions do
@moduledoc """
The Permissions context.
"""
def has_moderating_permission(brainstorming, current_user) when current_user != nil do
Enum.member?(current_user.moderated_brainstormings |> Enum.map(& &1.id), brainstorming.id)
end

def has_moderating_permission(_, _) do
false
end
end
2 changes: 1 addition & 1 deletion lib/mindwendel_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule MindwendelWeb do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: MindwendelWeb.Layouts]
layouts: [html: {MindwendelWeb.Layouts, :app_static}]

import Plug.Conn
use Gettext, backend: MindwendelWeb.Gettext
Expand Down
5 changes: 2 additions & 3 deletions lib/mindwendel_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ defmodule MindwendelWeb.Layouts do
Brainstormings.list_brainstormings_for(user.id, limit)
end

def admin_route(conn) do
route_scope = conn.request_path |> String.split("/", trim: true) |> List.first()
route_scope == "admin"
def admin_view(current_view) do
current_view == MindwendelWeb.Admin.BrainstormingLive.Edit
end
end
19 changes: 0 additions & 19 deletions lib/mindwendel_web/controllers/static_page_controller.ex
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
defmodule MindwendelWeb.StaticPageController do
use MindwendelWeb, :controller
alias Mindwendel.Brainstormings
alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.FeatureFlag

plug :put_root_layout, {MindwendelWeb.Layouts, :static_page}

def home(conn, _params) do
current_user =
conn
|> Mindwendel.Services.SessionService.get_current_user_id()
|> Mindwendel.Accounts.get_user()

form =
%Brainstorming{}
|> Brainstormings.change_brainstorming(%{})
|> Phoenix.Component.to_form()

render(conn, "home.html",
current_user: current_user,
form: form
)
end

def legal(conn, _params) do
if FeatureFlag.enabled?(:feature_privacy_imprint_enabled) do
render(conn, "legal.html")
Expand Down
12 changes: 0 additions & 12 deletions lib/mindwendel_web/controllers/static_page_html.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
defmodule MindwendelWeb.StaticPageHTML do
use MindwendelWeb, :html
alias Mindwendel.Brainstormings

embed_templates "static_page_html/*"

def list_brainstormings_for(user) do
Brainstormings.list_brainstormings_for(user.id)
end

def brainstormings_available_until() do
Timex.Duration.from_days(
Application.fetch_env!(:mindwendel, :options)[:feature_brainstorming_removal_after_days]
)
|> Timex.format_duration(:humanized)
end
end
1 change: 1 addition & 0 deletions lib/mindwendel_web/live/admin/brainstorming_live/edit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule MindwendelWeb.Admin.BrainstormingLive.Edit do
{
:ok,
socket
|> assign(:current_view, socket.view)
|> assign(:page_title, "Admin")
|> assign(:brainstorming, brainstorming)
|> assign(:form, to_form(changeset))
Expand Down
19 changes: 18 additions & 1 deletion lib/mindwendel_web/live/brainstorming_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
alias Mindwendel.Ideas
alias Mindwendel.Brainstormings.Idea
alias Mindwendel.Brainstormings.Lane
alias Mindwendel.LocalStorage

@impl true
def mount(%{"id" => id}, session, socket) do
Expand All @@ -33,6 +34,8 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
{
:ok,
socket
|> assign(:brainstormings_stored, [])
|> assign(:current_view, socket.view)
|> assign(:brainstorming, brainstorming)
|> assign(:lanes, lanes)
|> assign(:current_user, current_user)
Expand All @@ -42,8 +45,9 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
{:error, _} ->
{:ok,
socket
|> put_flash(:missing_brainstorming_id, id)
|> put_flash(:error, gettext("Brainstorming not found"))
|> push_navigate(to: "/")}
|> redirect(to: "/")}
end
end

Expand All @@ -59,6 +63,19 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
mount(%{"id" => id}, session, socket)
end

@impl true
def handle_event("brainstormings_from_local_storage", brainstormings_stored, socket) do
# Brainstormings are used from session data and local storage. Session data can be removed later and is only used for a transition period.
valid_stored_brainstormings =
LocalStorage.brainstormings_from_local_storage_and_session(
brainstormings_stored,
Brainstormings.list_brainstormings_for(socket.assigns.current_user.id),
socket.assigns.current_user
)

{:noreply, assign(socket, :brainstormings_stored, valid_stored_brainstormings)}
end

@impl true
def handle_params(
params,
Expand Down
13 changes: 12 additions & 1 deletion lib/mindwendel_web/live/brainstorming_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@
</div>
<% end %>

<div id="brainstorming-details">
<div
id="brainstorming-details"
data-id={@brainstorming.id}
data-name={@brainstorming.name}
data-last-accessed-at={@brainstorming.last_accessed_at}
data-admin-url-id={
if has_moderating_permission(@brainstorming, @current_user),
do: @brainstorming.admin_url_id,
else: nil
}
phx-hook="StoreRecentBrainstorming"
>
<div class="content">
<div class="row">
<div class="col-sm-12 col-md-12 col-xl-6">
Expand Down
10 changes: 9 additions & 1 deletion lib/mindwendel_web/live/live_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ defmodule MindwendelWeb.LiveHelpers do

alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.FeatureFlag
alias Mindwendel.Permissions

def has_move_permission(brainstorming, current_user) do
brainstorming.option_allow_manual_ordering or
has_moderating_permission(brainstorming, current_user)
end

def has_moderating_permission(brainstorming, current_user) do
Enum.member?(current_user.moderated_brainstormings |> Enum.map(& &1.id), brainstorming.id)
Permissions.has_moderating_permission(brainstorming, current_user)
end

def has_ownership(record, current_user) do
Expand All @@ -36,6 +37,13 @@ defmodule MindwendelWeb.LiveHelpers do
Brainstorming.brainstorming_available_until(brainstorming)
end

def brainstormings_available_until() do
Timex.Duration.from_days(
Application.fetch_env!(:mindwendel, :options)[:feature_brainstorming_removal_after_days]
)
|> Timex.format_duration(:humanized)
end

def show_idea_file_upload? do
FeatureFlag.enabled?(:feature_file_upload)
end
Expand Down
Loading

0 comments on commit 21d1680

Please sign in to comment.