Skip to content

Commit

Permalink
Feat/cart (#16)
Browse files Browse the repository at this point in the history
* feat: logic for handling cart items + tests

* fix: remove console log

* feat: Implementing GenServer, caching for cart + tests

* feat: cart item live view

* feat: add items to cart + tests
  • Loading branch information
ricksonoliveira authored Dec 14, 2023
1 parent c4e0cd7 commit d45d5c0
Show file tree
Hide file tree
Showing 25 changed files with 772 additions and 62 deletions.
2 changes: 1 addition & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import Hooks from "./hooks"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken, cart_id: sessionStorage.getItem("cart_id")}, hooks: Hooks})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
Expand Down
4 changes: 3 additions & 1 deletion assets/js/hooks.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import LoadMoreProducts from "./hooks/loadMoreProducts"
import CartSession from "./hooks/cartSession"

let Hooks = {
LoadMoreProducts: LoadMoreProducts
LoadMoreProducts: LoadMoreProducts,
CartSession: CartSession
}

export default Hooks
10 changes: 10 additions & 0 deletions assets/js/hooks/cartSession.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const CartSession = {
mounted(){
this.handleEvent("create_cart_session_id", map => {
var {cart_id: cart_id} = map;
sessionStorage.setItem("cart_id", cart_id);
})
}
}

export default CartSession;
1 change: 0 additions & 1 deletion assets/js/hooks/loadMoreProducts.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const LoadMoreProducts = {
this.observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
console.log("load more")
this.pushEventTo(selector, "load_more_products", {})
}
})
Expand Down
4 changes: 3 additions & 1 deletion lib/food_order/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule FoodOrder.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
alias FoodOrder.Carts.Server.CartSession

use Application

Expand All @@ -17,7 +18,8 @@ defmodule FoodOrder.Application do
# Start Finch
{Finch, name: FoodOrder.Finch},
# Start the Endpoint (http/https)
FoodOrderWeb.Endpoint
FoodOrderWeb.Endpoint,
CartSession
# Start a worker by calling: FoodOrder.Worker.start_link(arg)
# {FoodOrder.Worker, arg}
]
Expand Down
10 changes: 10 additions & 0 deletions lib/food_order/carts/carts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule FoodOrder.Carts do
@name :cart_session

def create(cart_id), do: GenServer.cast(@name, {:create, cart_id})
def add(cart_id, product), do: GenServer.cast(@name, {:add, cart_id, product})
def get(cart_id), do: GenServer.call(@name, {:get, cart_id})
def increment(cart_id, product_id), do: GenServer.call(@name, {:increment, cart_id, product_id})
def decrement(cart_id, product_id), do: GenServer.call(@name, {:decrement, cart_id, product_id})
def remove(cart_id, product_id), do: GenServer.call(@name, {:remove, cart_id, product_id})
end
156 changes: 156 additions & 0 deletions lib/food_order/carts/core/handle_carts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
defmodule FoodOrder.Carts.Core.HandleCarts do
alias FoodOrder.Carts.Data.Cart

@doc """
Create a new cart session
## Examples
iex> FoodOrder.Carts.Core.HandleCarts.create(444_444)
%Cart{
id: 444_444,
items: [],
total_price: %Money{amount: 0, currency: :USD},
total_qty: 0
}
"""
def create(cart_id) do
Cart.new(cart_id)
end

@doc """
Add a new item to the cart
## Examples
iex> FoodOrder.Carts.Core.HandleCarts.add(cart, product)
%Cart{
id: 444_444,
items: [%{item: product, qty: 1}],
total_price: product.price,
total_qty: 1
}
"""
def add(cart, item) do
new_total_price = Money.add(cart.total_price, item.price)
new_items = new_item(cart.items, item)

%{
cart
| total_qty: cart.total_qty + 1,
items: new_items,
total_price: new_total_price
}
end

defp new_item(items, item) do
is_there_item_id? = Enum.find(items, &(item.id == &1.item.id))

if is_there_item_id? == nil do
items ++ [%{item: item, qty: 1}]
else
items
|> Map.new(fn item -> {item.item.id, item} end)
|> Map.update!(item.id, &%{&1 | qty: &1.qty + 1})
|> Map.values()
end
end

@doc """
Remove an item from the cart
## Examples
iex> FoodOrder.Carts.Core.HandleCarts.remove(cart, product.id)
%Cart{
id: 444_444,
items: [%{item: product, qty: 1}],
total_price: product.price,
total_qty: 1
}
"""
def remove(cart, item_id) do
{items, item_removed} = Enum.reduce(cart.items, {[], nil}, &remove_item(&1, &2, item_id))
total_price_to_deduct = Money.multiply(item_removed.item.price, item_removed.qty)
total_price = Money.subtract(cart.total_price, total_price_to_deduct)

%{cart | items: items, total_qty: cart.total_qty - item_removed.qty, total_price: total_price}
end

defp remove_item(item, acc, item_id) do
{list, item_acc} = acc

if item.item.id == item_id do
{list, item}
else
{[item | list], item_acc}
end
end

@doc """
Increment an item from the cart
## Examples
iex> FoodOrder.Carts.Core.HandleCarts.increment(cart, product.id)
%Cart{
id: 444_444,
items: [%{item: product, qty: 2}],
total_price: product.price,
total_qty: 2
}
"""
def increment(%{items: items} = cart, item_id) do
{items_updated, product} =
Enum.reduce(items, {[], nil}, fn item_detail, acc ->
{list, item} = acc

if item_detail.item.id == item_id do
updated_item = %{item_detail | qty: item_detail.qty + 1}
item_updated = [updated_item]
{list ++ item_updated, updated_item}
else
{[item_detail | list], item}
end
end)

total_price = Money.add(cart.total_price, product.item.price)
%{cart | items: items_updated, total_qty: cart.total_qty + 1, total_price: total_price}
end

@doc """
Decrement an item from the cart
## Examples
iex> FoodOrder.Carts.Core.HandleCarts.decrement(cart, product.id)
%Cart{
id: 444_444,
items: [%{item: product, qty: 1}],
total_price: product.price,
total_qty: 1
}
"""
def decrement(%{items: items} = cart, item_id) do
{items_updated, product} =
Enum.reduce(items, {[], nil}, fn item_detail, acc ->
{list, item} = acc

if item_detail.item.id == item_id do
updated_item = %{item_detail | qty: item_detail.qty - 1}

if updated_item.qty == 0 do
{list, updated_item}
else
item_updated = [updated_item]
{list ++ item_updated, updated_item}
end
else
{[item_detail | list], item}
end
end)

total_price = Money.subtract(cart.total_price, product.item.price)
%{cart | items: items_updated, total_qty: cart.total_qty - 1, total_price: total_price}
end
end
8 changes: 8 additions & 0 deletions lib/food_order/carts/data/cart.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule FoodOrder.Carts.Data.Cart do
defstruct id: nil,
items: [],
total_price: Money.new(0),
total_qty: 0

def new(id), do: %__MODULE__{id: id}
end
64 changes: 64 additions & 0 deletions lib/food_order/carts/server/cart_session.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule FoodOrder.Carts.Server.CartSession do
use GenServer

import FoodOrder.Carts.Core.HandleCarts

@name :cart_session

def start_link(_), do: GenServer.start_link(__MODULE__, @name, name: @name)

def init(name) do
:ets.new(name, [:set, :public, :named_table])
{:ok, name}
end

def handle_cast({:create, cart_id}, name) do
case find_cart(name, cart_id) do
{:error, []} -> :ets.insert(name, {cart_id, create(cart_id)})
{:ok, _cart} -> :ok
end

{:noreply, name}
end

def handle_cast({:add, cart_id, product}, name) do
{:ok, cart} = find_cart(name, cart_id)
cart = add(cart, product)
:ets.insert(name, {cart_id, cart})

{:noreply, name}
end

def handle_call({:get, cart_id}, _from, name) do
{:ok, cart} = find_cart(name, cart_id)
{:reply, cart, name}
end

def handle_call({:increment, cart_id, product_id}, _from, name) do
{:ok, cart} = find_cart(name, cart_id)
cart = increment(cart, product_id)
:ets.insert(name, {cart_id, cart})
{:reply, cart, name}
end

def handle_call({:decrement, cart_id, product_id}, _from, name) do
{:ok, cart} = find_cart(name, cart_id)
cart = decrement(cart, product_id)
:ets.insert(name, {cart_id, cart})
{:reply, cart, name}
end

def handle_call({:remove, cart_id, product_id}, _from, name) do
{:ok, cart} = find_cart(name, cart_id)
cart = remove(cart, product_id)
:ets.insert(name, {cart_id, cart})
{:reply, cart, name}
end

defp find_cart(name, cart_id) do
case :ets.lookup(name, cart_id) do
[] -> {:error, []}
[{_cart_id, cart}] -> {:ok, cart}
end
end
end
10 changes: 6 additions & 4 deletions lib/food_order_web/components/layouts/header_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ defmodule FoodOrderWeb.HeaderComponent do
</.link>
</li>
</ul>
<a href={~p"/cart"} class="ml-6 p-6 bg-fuchsia-500 rounded-full text-neutral-100 flex group hover:text-fuchsia-500 hover:bg-fuchsia-300 transition">
<span class="text-xs">0</span>
<Heroicons.shopping_cart solid class="h-5 w-5 stroke-current" />
</a>
<% else %>
<li class="ml-6">
<.link href={~p"/users/register"}>
Expand All @@ -63,6 +59,12 @@ defmodule FoodOrderWeb.HeaderComponent do
</.link>
</li>
<% end %>
<%= if !is_nil(@cart_id) do %>
<a href={~p"/cart"} class="ml-6 p-6 bg-fuchsia-500 rounded-full text-neutral-100 flex group hover:text-fuchsia-500 hover:bg-fuchsia-300 transition">
<span class="text-xs"><%= FoodOrder.Carts.get(@cart_id).total_qty %></span>
<Heroicons.shopping_cart solid class="h-5 w-5 stroke-current" />
</a>
<% end %>
</ul>
</nav>
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/food_order_web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</head>
<body>
<div class="mx-auto max-w-7xl">
<FoodOrderWeb.HeaderComponent.menu current_user={@current_user} />
<FoodOrderWeb.HeaderComponent.menu current_user={@current_user} cart_id={Map.get(assigns, :cart_id)} request_path={@conn.request_path} />
<%= @inner_content %>
</div>
</body>
Expand Down
10 changes: 9 additions & 1 deletion lib/food_order_web/live/cart_live/cart_live.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
defmodule FoodOrderWeb.CartLive do
use FoodOrderWeb, :live_view

alias FoodOrder.Carts
alias FoodOrderWeb.CartLive.Details

def mount(_, _, socket) do
{:ok, assign(socket, total_quantity: 0)}
cart_id = socket.assigns.cart_id
cart = Carts.get(cart_id)
{:ok, assign(socket, cart: cart)}
end

def handle_info({:update, cart}, socket) do
{:noreply, assign(socket, cart: cart)}
end

defp empty_cart(assigns) do
Expand Down
4 changes: 2 additions & 2 deletions lib/food_order_web/live/cart_live/cart_live.html.heex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<section class="py-16" data-role="cart">
<%= if @total_quantity == 0 do %>
<%= if @cart.total_qty == 0 do %>
<.empty_cart />
<% else %>
<.live_component module={Details} id="cart-details" />
<.live_component module={Details} id="cart-details" cart={@cart} />
<% end %>
</section>
1 change: 1 addition & 0 deletions lib/food_order_web/live/cart_live/details/details.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
defmodule FoodOrderWeb.CartLive.Details do
use FoodOrderWeb, :live_component
alias __MODULE__.Item
end
Loading

0 comments on commit d45d5c0

Please sign in to comment.