Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/assets/images/icons/heart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/images/icons/heart_full.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ class ApplicationController < ActionController::Base
helper_method :signed_in?, :current_user

def sign_in!(user)
user.transaction do
user.last_login_at = Time.now
user.save!
end
UserLogin.create(user: user, login_at: Time.now)
session[:user_id] = user.id
end

Expand All @@ -26,4 +23,8 @@ def authenticate_user!
return redirect_to homepage_path(redirect_to: request.original_url)
end
end

def render_not_found
render file: Rails.public_path.join('404.html'), status: :not_found, layout: false
end
end
25 changes: 25 additions & 0 deletions app/controllers/favorite_products_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class FavoriteProductsController < ApplicationController
before_action :set_product

def create
if FavoriteProduct.create(product: @product, user: current_user)
flash[:success] = 'Product has been favorited'
else
flash[:error] = 'Something went wrong'
end

redirect_back(fallback_location: homepage_path)
end

def destroy
FavoriteProduct.where(product_id: @product.id, user_id: current_user.id).first.destroy
flash[:success] = 'Product has been deleted from favorites'
redirect_back(fallback_location: homepage_path)
end

private

def set_product
@product = Product.active.find(params[:product_id] || params[:id])
end
end
7 changes: 3 additions & 4 deletions app/controllers/shop_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

class ShopController < ApplicationController

def category
category_slug = params.require([:category_slug])
@category = Category.find_by!(slug: category_slug)
Expand All @@ -9,7 +7,8 @@ def category
def product
category_slug, product_slug = params.require([:category_slug, :product_slug])
@category = Category.find_by!(slug: category_slug)
@product = @category.products.find_by!(slug: product_slug)
end
@product = @category.products.find_by(slug: product_slug)

render_not_found unless @product&.is_active?
end
end
7 changes: 7 additions & 0 deletions app/models/favorite_product.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class FavoriteProduct < ApplicationRecord
validates_presence_of :product_id, :user_id
validates_uniqueness_of :product_id, scope: :user_id

belongs_to :user
belongs_to :product
end
2 changes: 1 addition & 1 deletion app/models/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
class Product < ApplicationRecord
validates_presence_of :name, :slug, :price_usd, :image_url
validates_uniqueness_of :slug

has_many :product_categories
has_many :categories, through: :product_categories

Expand Down
5 changes: 4 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class User < ApplicationRecord
validates :password, presence: true
has_secure_password

has_many :user_logins
has_many :favorite_products
has_many :favorites, through: :favorite_products, source: :product

def password
@password ||= Password.new(password_digest)
end
Expand All @@ -17,5 +21,4 @@ def password=(new_password)
@password = Password.create(new_password)
self.password_digest = @password
end

end
7 changes: 7 additions & 0 deletions app/models/user_login.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class UserLogin < ApplicationRecord
belongs_to :user

validates_presence_of :user_id, :login_at

scope :ordered, -> { order(login_at: :desc) }
end
21 changes: 16 additions & 5 deletions app/views/members/dashboard.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<div class="flex flex-col mt-10 mb-10">
<h1 class="text-4xl">Member Dashboard</h1>
<div class="border-t border-gray-400 w-full"></div>
Expand All @@ -7,19 +6,31 @@
<div class="m-2 border border-gray-400 shadow-md rounded-md p-4">
<h2 class="text-xl font-bold">Profile Details:</h2>
<ul>
<li>Email: ...</li>
<li>Created At: ...</li>
<li>Updated At: ...</li>
<li>Email: <%= current_user.email %></li>
<li>Created At: <%= current_user.created_at %></li>
<li>Updated At: <%= current_user.updated_at %></li>
</ul>
</div>
</div>
<div class="flex flex-col w-1/2 h-full">
<div class="m-2 border border-gray-400 shadow-md rounded-md p-4">
<h2 class="text-xl font-bold">Login History:</h2>
<ul>
<li><%= current_user.last_login_at %></li>
<% current_user.user_logins.ordered.each do |login| %>
<li><%= login.login_at %></li>
<% end %>
</ul>
</div>
</div>
</div>

<% current_user.favorites.active.any? do %>
<div class="flex flex-row">
<div class="m-2 border border-gray-400 shadow-md rounded-md p-4 w-full">
<h2 class="text-xl font-bold">Favorite products:</h2>

<%= render partial: "shared/product_small_card", collection: current_user.favorites.active, as: :product %>
</div>
</div>
<% end %>
</div>
13 changes: 13 additions & 0 deletions app/views/shared/_favorite_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="w-5">
<% if signed_in? %>
<% if current_user.favorite_products.exists?(product_id: product.id) %>
<%= button_to favorite_product_path(product), method: :delete do %>
<%= image_tag "icons/heart_full.svg", class: "w-full", alt: "heart icon" %>
<% end %>
<% else %>
<%= button_to favorite_products_path(product_id: product) do %>
<%= image_tag "icons/heart.svg", class: "w-full", alt: "heart icon" %>
<% end %>
<% end %>
<% end %>
</div>
15 changes: 15 additions & 0 deletions app/views/shared/_product_small_card.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div
class="flex flex-row items-center my-4 rounded-lg bg-white shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)]"
>
<img class="w-28 rounded-l-lg" src="<%= image_url product.image_url %>" alt="Photo of <%= product.name %>" />
<div class="flex flex-row justify-between w-full">
<p class="text-md font-medium leading-tight text-neutral-800 ml-4">
<a href="<%= product_path(category_slug: product.categories.first.slug, product_slug: product.slug) %>">
<%= product.name %>
</a>
</p>
<div class="mr-4">
<%= render "shared/favorite_button", product: product %>
</div>
</div>
</div>
16 changes: 10 additions & 6 deletions app/views/shop/category.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@
<%= @category.name %>
</div>
<div class="flex flex-row flex-wrap">
<% @category.products.each do |product| %>
<% @category.products.active.each do |product| %>
<div
class="block w-1/2 my-4 px-3 rounded-lg bg-white shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] ">
class="block w-1/2 my-4 px-3 pt-3 rounded-lg bg-white shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)]">
<a href="<%= product_path(category_slug: @category.slug, product_slug: product.slug) %>">
<img
class="rounded-t-lg"
src="<%= image_url product.image_url %>" alt="Photo of <%= product.name %>"
/>
</a>
<div class="p-6">
<h5
class="mb-2 text-xl font-medium leading-tight text-neutral-800 ">
<%= product.name %>
</h5>
<div class="flex flex-row justify-between">
<h5
class="mb-2 text-xl font-medium leading-tight text-neutral-800 ">
<%= product.name %>
</h5>
<%= render "shared/favorite_button", product: product %>
</div>

<p class="mb-4 text-base text-neutral-600 ">
<%= product.description %>
</p>
Expand Down
21 changes: 16 additions & 5 deletions app/views/shop/product.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,27 @@
</div>
<div class="flex flex-row flex-wrap">
<div
class="block w-full mx-auto my-4 px-3 rounded-lg bg-white shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] ">
class="block w-full mx-auto my-4 px-3 rounded-lg bg-white shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)]">
<img
class="rounded-t-lg"
src="<%= image_url @product.image_url %>" alt="Photo of <%= @product.name %>"
/>
<div class="p-6">
<h5
class="mb-2 text-xl font-medium leading-tight text-neutral-800 ">
<%= @product.name %>
</h5>
<div class="flex flex-row justify-between items-center mb-2">
<h5
class="mb-2 text-xl font-medium leading-tight text-neutral-800 ">
<%= @product.name %>
</h5>
<%= render "shared/favorite_button", product: @product %>
</div>
<div class="flex flex-row">
<% @product.categories.each do |category| %>
<p
class="text-sm bg-sky-100 rounded mb-4 mr-2 px-2 py-2 hover:bg-sky-200 active:bg-sky-800">
<a href="<%= category_path(category_slug: category.slug) %>"><%= category.name %></a>
</p>
<% end %>
</div>
<p class="mb-4 text-base text-neutral-600 ">
<%= number_to_currency(@product.price_usd) %>
</p>
Expand Down
7 changes: 5 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Defines the root path route ("/")
# root "articles#index"
root "public#homepage", as: :homepage
root 'public#homepage', as: :homepage

scope '', format: false do
get '/logout', to: 'signin#logout', as: :logout
Expand All @@ -24,6 +24,9 @@
scope 'members' do
get '', to: 'members#dashboard', as: :members_dashboard
end
end

resources :favorite_products, only: %i[create destroy]

match '*unmatched', to: 'application#not_found_method', via: :all
end
end
31 changes: 31 additions & 0 deletions db/migrate/20230817061119_add_user_logins.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class AddUserLogins < ActiveRecord::Migration[7.0]
def up
create_table :user_logins do |t|
t.references :user
t.datetime :login_at, null: false
end

execute <<-SQL
INSERT INTO user_logins (user_id, login_at)
SELECT id, last_login_at FROM users;
SQL
Comment on lines +8 to +11
Copy link
Collaborator Author

@olenaniemova olenaniemova Aug 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, it is not the best solution to put raw SQL in the migration to migrate data from one table to another. But it works in this small project.
For real projects which have a big codebase and database, in such cases, I would use a data-migrate gem or rake task to create separate migration for data. The development should look like this:

  1. Create migration for the new table, create migration for data, change code to save data into the new table
  2. Test, release to prod, and check that all work well
  3. Create migration to remove the old column
  4. Test, release changes


remove_column :users, :last_login_at, :datetime
end

def down
add_column :users, :last_login_at, :datetime

execute <<-SQL
UPDATE users
SET last_login_at = (
SELECT login_at FROM user_logins
WHERE user_logins.user_id = users.id
ORDER BY login_at DESC
LIMIT 1
);
SQL

drop_table :user_logins
end
end
10 changes: 10 additions & 0 deletions db/migrate/20230818074506_add_favorite_products.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AddFavoriteProducts < ActiveRecord::Migration[7.0]
def change
create_table :favorite_products do |t|
t.references :product, index: true
t.references :user, index: true

t.timestamps
end
end
end
18 changes: 16 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.