diff --git a/app/assets/images/plays_hub_logo.svg b/app/assets/images/plays_hub_logo.svg new file mode 100644 index 0000000..d30c7ba --- /dev/null +++ b/app/assets/images/plays_hub_logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/assets/stylesheets/app.css b/app/assets/stylesheets/app.css index 550ae83..750f2e1 100644 --- a/app/assets/stylesheets/app.css +++ b/app/assets/stylesheets/app.css @@ -14,8 +14,200 @@ main { padding-left: 3vw; padding-right: 3vw; box-sizing: border-box; + margin-top: 4.5em; } +.dashboard-widget { + background: #fff; border-radius: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); padding: 2em 2.5em; +} + +.dashboard-widget .widget-title { + margin-top: 0; + font-size: 1.4em; + color: #0071e3; + font-weight: 600; + margin-bottom: 1.2em; + letter-spacing: 0.5px; + text-align: left; + width: 100%; +} + +.dashboard-widget .widget-content { + margin-top: 1em; +} + +.dashboard-widget .widget-content div { + margin-bottom: 0.5em; +} + +.dashboard-widget .widget-content div strong { + font-weight: 600; +} + +.site-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5em 3vw; + background: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + position: sticky; + top: 0; + z-index: 100; +} + +.site-header .logo a { + font-weight: bold; + font-size: 1.4em; + color: #0071e3; + text-decoration: none; +} + +.site-header nav a { + margin-left: 2em; + color: #222; + text-decoration: none; + font-weight: 500; + transition: color 0.15s; +} + +.site-header nav a:hover { + color: #0071e3; +} + +.site-header .logout-btn { + color: #e00; + font-weight: 600; + background: none; + border: none; + padding: 0; + margin-left: 2em; + font-size: 1em; + cursor: pointer; + text-decoration: none; + box-shadow: none; + display: inline; +} + +.site-header .logout-link-btn { + background: none; + border: none; + color: #e00; + font-weight: 600; + padding: 0; + margin-left: 2em; + font-size: 1em; + cursor: pointer; + text-decoration: none; + box-shadow: none; + display: inline; +} + +.site-header .logout-link-btn:hover { + text-decoration: underline; + color: #b00; +} + + +@media (max-width: 700px) { + .site-header { + flex-direction: column; + align-items: flex-start; + padding: 0.7em 1vw; + } + .site-header nav a { + margin-left: 1em; + font-size: 1em; + } + main { + margin-top: 5.5em; + } +} + +.login-card { + background: #fff; + border-radius: 16px; + box-shadow: 0 2px 12px rgba(0,0,0,0.06); + padding: 2em 2.5em; + max-width: 400px; + margin: 3em auto 2.5em auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + + +.login-heading { + margin-top: 0; + font-size: 1.4em; + color: #0071e3; + font-weight: 600; + margin-bottom: 1.2em; + letter-spacing: 0.5px; + text-align: left; + width: 100%; +} + +.login-card .field { + width: 100%; + margin-bottom: 1.2em; +} + +.login-card .actions { + width: 100%; + display: flex; + justify-content: flex-start; +} + +.login-card .actions input[type="submit"], +.login-card .actions button, +.login-card button[type="submit"] { + background: #0071e3; + color: #fff; + border: none; + border-radius: 12px; + padding: 0.8em 2.2em; + font-size: 1.05em; + font-weight: 600; + cursor: pointer; + opacity: 1; + margin-top: 1em; + transition: background 0.2s, opacity 0.2s; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + letter-spacing: 0.2px; +} + +.login-card .actions input[type="submit"]:hover, +.login-card .actions button:hover, +.login-card button[type="submit"]:hover { + background: #005bb5; +} + + +.login-card input[type="email"], +.login-card input[type="password"] { + width: 100%; + padding: 0.7em 1em; + border-radius: 8px; + border: 1px solid #e0e0e0; + font-size: 1em; + margin-top: 0.3em; + box-sizing: border-box; +} + +.login-card input[type="email"]:focus, +.login-card input[type="password"]:focus { + outline: none; + border-color: #0071e3; + box-shadow: 0 0 0 2px #e8f0fe; +} + +.login-card label { + font-weight: 500; + color: #222; +} + + @media (max-width: 700px) { main { padding-left: 1vw; @@ -77,7 +269,6 @@ button { font-weight: 600; cursor: not-allowed; opacity: 0.7; - margin-top: 2em; transition: background 0.2s, opacity 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.08); letter-spacing: 0.2px; diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb deleted file mode 100644 index 877cc2e..0000000 --- a/app/controllers/home_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class HomeController < ApplicationController - before_action :authenticate_user! - - def index - @user = current_user - @logins = Login.where(user: @user) - end -end diff --git a/app/controllers/statistics_controller.rb b/app/controllers/statistics_controller.rb index 3400644..895c42b 100644 --- a/app/controllers/statistics_controller.rb +++ b/app/controllers/statistics_controller.rb @@ -4,10 +4,12 @@ class StatisticsController < ApplicationController def index stats = Statistics.new(current_user) @total_plays = stats.total_plays + @year_plays = stats.total_plays_year @activity_by_day = stats.activity_by_day @top_artists_all_time = stats.top_artists_all_time @top_artists_year = stats.top_artists_year @top_artists_upcoming = stats.top_artists_upcoming @since_date = stats.since_date + @streak = stats.longest_streak end end diff --git a/app/models/statistics.rb b/app/models/statistics.rb index f441adf..8106832 100644 --- a/app/models/statistics.rb +++ b/app/models/statistics.rb @@ -9,6 +9,11 @@ class Statistics Activity.where(user: user).count end + def total_plays_year + year_start = Date.today.beginning_of_year + Activity.where(user: user, created_at: year_start..Date.today.end_of_day).count + end + def activity_by_day start_date = 1.year.ago.to_date end_date = Date.today @@ -37,4 +42,37 @@ class Statistics def since_date Activity.where(user: user).order(:created_at).limit(1).pick(:created_at) end + + # Returns a hash with streak info: :start_date, :end_date, :length, :total_plays, :most_played_song, :most_played_song_count + def longest_streak + days = activity_by_day.keys.map { |d| Date.parse(d) }.sort + return nil if days.empty? + streaks = [] + current_streak = [] + days.each_with_index do |day, i| + if i == 0 || day == days[i-1] + 1 + current_streak << day + else + streaks << current_streak + current_streak = [day] + end + end + streaks << current_streak unless current_streak.empty? + best = streaks.max_by(&:length) + return nil unless best && best.length > 0 + streak_start = best.first + streak_end = best.last + plays = Activity.where(user: user, created_at: streak_start.beginning_of_day..streak_end.end_of_day) + total_plays = plays.count + song_counts = plays.group(:item_title).order('count_id DESC').limit(1).count(:id) + most_played_song, most_played_song_count = song_counts.first || [nil, 0] + { + start_date: streak_start, + end_date: streak_end, + length: best.length, + total_plays: total_plays, + most_played_song: most_played_song, + most_played_song_count: most_played_song_count + } + end end diff --git a/app/models/user.rb b/app/models/user.rb index 4756799..1ad2ddc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,4 +3,8 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + def is_admin? + id == 1 + end end diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb index 9b486b8..df33663 100644 --- a/app/views/devise/passwords/new.html.erb +++ b/app/views/devise/passwords/new.html.erb @@ -1,16 +1,16 @@ -

Forgot your password?

- -<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> - <%= render "devise/shared/error_messages", resource: resource %> - -
- <%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+
+ <%= f.submit "Send me reset password instructions" %> +
+ <% end %> +
+ <%= render "devise/shared/links" %>
- -
- <%= f.submit "Send me reset password instructions" %> -
-<% end %> - -<%= render "devise/shared/links" %> +
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index d655b66..2c1d596 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1,29 +1,27 @@ -

Sign up

- -<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> - <%= render "devise/shared/error_messages", resource: resource %> - -
- <%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 5ede964..118d8cd 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,26 +1,28 @@ -

Log in

- -<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> -
- <%= f.label :email %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> -
- -
- <%= f.label :password %>
- <%= f.password_field :password, autocomplete: "current-password" %> -
- - <% if devise_mapping.rememberable? %> +