diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c9879935..af42847e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -39,6 +39,7 @@ @import "overrides/alerts", +"overrides/badges", "overrides/bootstrap-datetimepicker", "overrides/buttons", "overrides/colors", @@ -81,6 +82,7 @@ "components/inbox-entry", "components/jumbotron", "components/locales", +"components/mobile-nav", "components/notifications", "components/profile", "components/question", diff --git a/app/assets/stylesheets/components/_buttons.scss b/app/assets/stylesheets/components/_buttons.scss index 142b73fb..6550b4d8 100644 --- a/app/assets/stylesheets/components/_buttons.scss +++ b/app/assets/stylesheets/components/_buttons.scss @@ -11,7 +11,7 @@ text-transform: uppercase; text-decoration: none; position: fixed; - bottom: 0px; + bottom: unquote('calc(#{$navbar-height} + env(safe-area-inset-bottom))'); right: 0px; margin-right: 7px; margin-bottom: 7px; diff --git a/app/assets/stylesheets/components/_container.scss b/app/assets/stylesheets/components/_container.scss index 08d9bf08..701a5517 100644 --- a/app/assets/stylesheets/components/_container.scss +++ b/app/assets/stylesheets/components/_container.scss @@ -1,4 +1,10 @@ .container--main { padding-top: map-get($spacers, 3); padding-bottom: map-get($spacers, 3); + // Sass doesn't know about the safe-area-inset-* env vars and throws a syntax error + // We can get around this by using unquote() + // Sass also has its own built-in max() function which is not the same as the CSS one + // In order to use the correct one we can write it as Max() instead + padding-left: unquote('Max(15px, env(safe-area-inset-left))'); + padding-right: unquote('Max(15px, env(safe-area-inset-right))'); } \ No newline at end of file diff --git a/app/assets/stylesheets/components/_mobile-nav.scss b/app/assets/stylesheets/components/_mobile-nav.scss new file mode 100644 index 00000000..e95b65c3 --- /dev/null +++ b/app/assets/stylesheets/components/_mobile-nav.scss @@ -0,0 +1,36 @@ +#rs-mobile-nav { + .container { + padding: 0; + } + + padding: 4px 0 unquote('calc(env(safe-area-inset-bottom) + 4px)') 0; + + .navbar-icon-row { + flex-direction: row; + justify-content: space-around; + width: 100%; + + .nav-link { + padding: 0; + + .fa { + padding-top: 8px; + font-size: 20px; + } + + .badge { + position: absolute; + top: 4px; + transform: translateX(16px); + } + } + } +} + +#rs-mobile-nav-profile { + position: fixed; + bottom: unquote("calc(env(safe-area-inset-bottom) + #{$navbar-height + 2px})"); + right: unquote("calc(env(safe-area-inset-right) + 15px)"); + left: unset; + top: unset; +} diff --git a/app/assets/stylesheets/elements/_body.scss b/app/assets/stylesheets/elements/_body.scss index 92aa0578..9620b16d 100644 --- a/app/assets/stylesheets/elements/_body.scss +++ b/app/assets/stylesheets/elements/_body.scss @@ -3,5 +3,15 @@ body { word-wrap: break-word; color: RGB(var(--body-text)); background-color: var(--background); - padding-top: $navbar-height; + @include media-breakpoint-up('lg') { + padding-top: $navbar-height; + } + @include media-breakpoint-down('md') { + padding-bottom: $navbar-height; + } + + &.not-logged-in { + padding-top: $navbar-height; + padding-bottom: 0; + } } \ No newline at end of file diff --git a/app/assets/stylesheets/overrides/_badges.scss b/app/assets/stylesheets/overrides/_badges.scss new file mode 100644 index 00000000..3fa32589 --- /dev/null +++ b/app/assets/stylesheets/overrides/_badges.scss @@ -0,0 +1,6 @@ +@each $color in $color-names { + .badge-#{$color} { + color: var(--#{$color}); + background-color: RGB(var(--#{$color}-text)); + } +} \ No newline at end of file diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cc354420..75360767 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -16,14 +16,17 @@ module ApplicationHelper ].compact.join(" ") unless options[:icon].nil? - body = "#{content_tag(:i, '', class: "mdi-#{options[:icon]}")} #{body}" + if options[:icon_only] + body = "#{content_tag(:i, '', class: "fa fa-#{options[:icon]}", title: body)}" + else + body = "#{content_tag(:i, '', class: "fa fa-#{options[:icon]}")} #{body}" + end end unless options[:badge].nil? - # TODO: make this prettier? - body << " #{ - content_tag(:span, options[:badge], class: ("badge#{ - " badge-#{options[:badge_color]}" unless options[:badge_color].nil? - }"))}" + badge_class = "badge" + badge_class << " badge-#{options[:badge_color]}" unless options[:badge_color].nil? + badge_class << " badge-pill" if options[:badge_pill] + body += " #{content_tag(:span, options[:badge], class: badge_class)}" end content_tag(:li, link_to(body.html_safe, path, class: "nav-link"), class: classes) diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 61015cb2..2ba9d186 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -50,12 +50,21 @@ module ThemeHelper def theme_color theme = get_active_theme if theme - "##{get_hex_color_from_theme_value(theme.primary_color)}" + theme.theme_color else '#5e35b1' end end + def mobile_theme_color + theme = get_active_theme + if theme + theme.mobile_theme_color + else + '#f0edf4' + end + end + def get_active_theme if @user&.theme if user_signed_in? diff --git a/app/models/theme.rb b/app/models/theme.rb index bcdc5d05..b627fe97 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -20,4 +20,8 @@ class Theme < ApplicationRecord def theme_color ('#' + ('0000000' + primary_color.to_s(16))[-6, 6]) end + + def mobile_theme_color + ('#' + ('0000000' + background_color.to_s(16))[-6, 6]) + end end diff --git a/app/views/announcement/edit.haml b/app/views/announcement/edit.haml index e0a4c43b..840877ed 100644 --- a/app/views/announcement/edit.haml +++ b/app/views/announcement/edit.haml @@ -1,5 +1,5 @@ - provide(:title, generate_title('Edit announcement')) -.container.container--main +.container-lg.container--main .card .card-body = bootstrap_form_for(@announcement, url: { action: 'update' }, method: 'PATCH') do |f| diff --git a/app/views/announcement/index.haml b/app/views/announcement/index.haml index 1cf04dda..40aa7f8f 100644 --- a/app/views/announcement/index.haml +++ b/app/views/announcement/index.haml @@ -1,5 +1,5 @@ - provide(:title, generate_title('Announcements')) -.container.container--main +.container-lg.container--main - @announcements.each do |announcement| .card .card-body diff --git a/app/views/announcement/new.haml b/app/views/announcement/new.haml index a4495bc7..3363c72b 100644 --- a/app/views/announcement/new.haml +++ b/app/views/announcement/new.haml @@ -1,5 +1,5 @@ - provide(:title, generate_title('Add new announcement')) -.container.container--main +.container-lg.container--main .card .card-body = bootstrap_form_for(@announcement, url: { action: 'create' }) do |f| diff --git a/app/views/answer/show.haml b/app/views/answer/show.haml index 90888280..5f84d47d 100644 --- a/app/views/answer/show.haml +++ b/app/views/answer/show.haml @@ -1,4 +1,4 @@ - provide(:title, answer_title(@answer)) - provide(:og, answer_opengraph(@answer)) -.container.container--main +.container-lg.container--main = render 'answerbox', a: @answer, display_all: @display_all diff --git a/app/views/layouts/base.haml b/app/views/layouts/base.haml index 85b623d0..b7c0e155 100644 --- a/app/views/layouts/base.haml +++ b/app/views/layouts/base.haml @@ -3,8 +3,12 @@ %head %meta{ charset: 'utf-8' } %meta{ 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' } - %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no' } - %meta{ name: 'theme-color', content: theme_color } + %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover' } + - if user_signed_in? + %meta{ name: 'theme-color', content: theme_color, media: '(min-width: 993px)' } + %meta{ name: 'theme-color', content: mobile_theme_color, media: '(max-width: 992px)' } + - else + %meta{ name: 'theme-color', content: theme_color } %link{ rel: 'apple-touch-icon', href: '/apple-touch-icon-precomposed.png' } %link{ rel: 'icon', href: '/images/favicon/favicon-16.png', sizes: '16x16' } %link{ rel: 'icon', href: '/icon-152.png', sizes: '152x152' } @@ -19,7 +23,7 @@ = csrf_meta_tags = yield(:og) = yield(:meta) - %body + %body{ class: user_signed_in? ? '' : 'not-logged-in' } - if user_signed_in? = render 'navigation/main' - else diff --git a/app/views/layouts/feed.haml b/app/views/layouts/feed.haml index a62654f7..94434e16 100644 --- a/app/views/layouts/feed.haml +++ b/app/views/layouts/feed.haml @@ -1,4 +1,4 @@ -.container.container--main +.container-lg.container--main .row .col-md-3.col-sm-4.d-none.d-sm-block = render 'shared/sidebar' diff --git a/app/views/layouts/inbox.haml b/app/views/layouts/inbox.haml index 8dcff72c..8b82f9d6 100644 --- a/app/views/layouts/inbox.haml +++ b/app/views/layouts/inbox.haml @@ -1,4 +1,4 @@ -.container.container--main +.container-lg.container--main .row .col-md-3.col-xs-12.col-sm-4.order-2.order-sm-1 = render 'inbox/sidebar', delete_id: @delete_id, disabled: @disabled, inbox_count: @inbox_count diff --git a/app/views/layouts/moderation.haml b/app/views/layouts/moderation.haml index 46954223..22a48339 100644 --- a/app/views/layouts/moderation.haml +++ b/app/views/layouts/moderation.haml @@ -1,5 +1,5 @@ = render 'navigation/moderation' -.container.container--main +.container-lg.container--main .row .col-md-3.col-sm-4.col-xs-12 = render 'tabs/moderation' diff --git a/app/views/layouts/notifications.haml b/app/views/layouts/notifications.haml index f495ae8e..37225c3e 100644 --- a/app/views/layouts/notifications.haml +++ b/app/views/layouts/notifications.haml @@ -1,5 +1,5 @@ = render 'navigation/notification' -.container.container--main +.container-lg.container--main .row .col-md-3.col-xs-12.col-sm-4 = render 'tabs/notifications' diff --git a/app/views/layouts/user/settings.haml b/app/views/layouts/user/settings.haml index 77e5a5f6..5f238bab 100644 --- a/app/views/layouts/user/settings.haml +++ b/app/views/layouts/user/settings.haml @@ -1,4 +1,4 @@ -.container.container--main +.container-lg.container--main .row .col-md-3.col-xs-12.col-sm-4 = render 'tabs/settings' diff --git a/app/views/navigation/_desktop.haml b/app/views/navigation/_desktop.haml new file mode 100644 index 00000000..485634cd --- /dev/null +++ b/app/views/navigation/_desktop.haml @@ -0,0 +1,20 @@ +%nav.navbar.navbar-themed.navbar-expand-lg.bg-primary.fixed-top.d-lg-block.d-none{ role: :navigation } + .container{ class: ios_web_app? ? 'ios-web-app' : '' } + %a.navbar-brand{ href: '/' } + = APP_CONFIG['site_name'] + %ul.nav.navbar-nav.mr-auto + = nav_entry t('views.navigation.timeline'), root_path, icon: 'home' + = nav_entry t('views.navigation.inbox'), '/inbox', icon: 'inbox', badge: inbox_count + - if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod? + = nav_entry t('views.navigation.discover'), discover_path, icon: 'compass' + %ul.nav.navbar-nav + - if @user.present? && @user != current_user + %li.nav-item.d-none.d-sm-block{ data: { toggle: 'tooltip', placement: 'bottom' }, title: t('views.actions.list') } + %a.nav-link{ href: '#', data: { target: '#modal-list-memberships', toggle: :modal } } + %i.fa.fa-list.hidden-xs + %span.d-none.d-sm-inline.d-md-none= t('views.actions.list') + = render 'navigation/main/notifications' + %li.nav-item.d-none.d-sm-block{ data: { toggle: 'tooltip', placement: 'bottom' }, title: t('views.actions.ask_question') } + %a.nav-link{ href: '#', name: 'toggle-all-ask', data: { target: '#modal-ask-followers', toggle: :modal } } + %i.fa.fa-pencil-square-o + = render 'navigation/main/profile' diff --git a/app/views/navigation/_main.haml b/app/views/navigation/_main.haml index c08b1df9..3d6398fd 100644 --- a/app/views/navigation/_main.haml +++ b/app/views/navigation/_main.haml @@ -1,26 +1,5 @@ -%nav.navbar.navbar-themed.navbar-expand-lg.bg-primary.fixed-top{ role: :navigation } - .container{ class: ios_web_app? ? 'ios-web-app' : '' } - %a.navbar-brand{ href: '/' }= APP_CONFIG['site_name'] - %button.navbar-toggler{ data: { target: '#j2-main-navbar-collapse', toggle: :collapse }, type: :button } - %span.sr-only Toggle navigation - %span.navbar-toggler-icon - .collapse.navbar-collapse#j2-main-navbar-collapse - %ul.nav.navbar-nav.mr-auto - = nav_entry t('views.navigation.timeline'), root_path - = nav_entry t('views.navigation.inbox'), '/inbox', badge: inbox_count - - if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod? - = nav_entry t('views.navigation.discover'), discover_path - %ul.nav.navbar-nav - - if @user.present? && @user != current_user - %li.nav-item.d-none.d-sm-block{ data: { toggle: 'tooltip', placement: 'bottom' }, title: t('views.actions.list') } - %a.nav-link{ href: '#', data: { target: '#modal-list-memberships', toggle: :modal } } - %i.fa.fa-list.hidden-xs - %span.d-none.d-sm-inline.d-md-none= t('views.actions.list') - = render 'navigation/main/notifications' - %li.nav-item.d-none.d-sm-block{ data: { toggle: 'tooltip', placement: 'bottom' }, title: t('views.actions.ask_question') } - %a.nav-link{ href: '#', name: 'toggle-all-ask', data: { target: '#modal-ask-followers', toggle: :modal } } - %i.fa.fa-pencil-square-o - = render 'navigation/main/profile' += render 'navigation/desktop' += render 'navigation/mobile' = render 'modal/ask' %button.btn.btn-primary.btn-fab.d-block.d-sm-none{ data: { target: '#modal-ask-followers', toggle: :modal }, type: 'button' } diff --git a/app/views/navigation/_mobile.haml b/app/views/navigation/_mobile.haml new file mode 100644 index 00000000..5dd42539 --- /dev/null +++ b/app/views/navigation/_mobile.haml @@ -0,0 +1,17 @@ += render 'navigation/mobile/profile' +- notifications_icon = notification_count.nil? ? 'bell-o' : 'bell' +%nav.navbar.navbar-themed.bg-primary.fixed-bottom.d-lg-none.d-block#rs-mobile-nav{ role: :navigation } + .container{ class: ios_web_app? ? 'ios-web-app' : '' } + %ul.nav.navbar-nav.navbar-icon-row + = nav_entry t('views.navigation.timeline'), root_path, icon: 'home', icon_only: true + = nav_entry t('views.navigation.inbox'), '/inbox', + badge: inbox_count, badge_color: 'primary', badge_pill: true, + icon: 'inbox', icon_only: true + - if APP_CONFIG.dig(:features, :discover, :enabled) || current_user.mod? + = nav_entry t('views.navigation.discover'), discover_path, icon: 'compass', icon_only: true + = nav_entry t('views.navigation.notifications'), '/notifications', + badge: notification_count, badge_color: 'primary', badge_pill: true, + icon: notifications_icon, icon_only: true + %li.nav-item.profile--image-dropdown + %a.nav-link{ href: '#', data: { toggle: 'dropdown', target: '#rs-mobile-nav-profile' }, aria: { controls: 'rs-mobile-nav-profile', expanded: 'false' } } + %img.avatar-md.d-inline{ src: current_user.profile_picture.url(:small) } diff --git a/app/views/navigation/mobile/_profile.haml b/app/views/navigation/mobile/_profile.haml new file mode 100644 index 00000000..f4177cde --- /dev/null +++ b/app/views/navigation/mobile/_profile.haml @@ -0,0 +1,31 @@ +.dropdown-menu#rs-mobile-nav-profile + %h6.dropdown-header.d-none.d-sm-block= current_user.screen_name + %a.dropdown-item{ href: show_user_profile_path(current_user.screen_name) } + %i.fa.fa-fw.fa-user + = t('views.navigation.show') + %a.dropdown-item{ href: edit_user_registration_path } + %i.fa.fa-fw.fa-cog + = t('views.navigation.settings') + .dropdown-divider + - if current_user.has_role?(:administrator) + %a.dropdown-item{ href: rails_admin_path } + %i.fa.fa-fw.fa-cogs + = t('views.navigation.admin') + %a.dropdown-item{ href: sidekiq_web_path } + %i.fa.fa-fw.fa-bar-chart + = t('views.navigation.sidekiq') + %a.dropdown-item{ href: pghero_path } + %i.fa.fa-fw.fa-database + Database Monitor + %a.dropdown-item{ href: announcement_index_path } + %i.fa.fa-fw.fa-info + Announcements + .dropdown-divider + - if current_user.mod? + %a.dropdown-item{ href: moderation_path } + %i.fa.fa-fw.fa-gavel + = t('views.navigation.moderation') + .dropdown-divider + %a.dropdown-item{ href: destroy_user_session_path, data: { method: :delete } } + %i.fa.fa-fw.fa-sign-out + = t 'views.sessions.destroy' diff --git a/app/views/settings/security/recovery_keys.haml b/app/views/settings/security/recovery_keys.haml index c09e9164..fe7174df 100644 --- a/app/views/settings/security/recovery_keys.haml +++ b/app/views/settings/security/recovery_keys.haml @@ -1,4 +1,4 @@ -.container.container--main +.container-lg.container--main .row.justify-content-center .col-md-5.totp-setup__recovery-container .card diff --git a/app/views/static/privacy_policy.haml b/app/views/static/privacy_policy.haml index 5e99850c..0f51121d 100644 --- a/app/views/static/privacy_policy.haml +++ b/app/views/static/privacy_policy.haml @@ -1,5 +1,5 @@ - provide(:title, generate_title('Privacy Policy')) -.container.container--main +.container-lg.container--main .card .card-body = raw_markdown_io 'service-docs/en/policy/privacy.md' diff --git a/app/views/static/terms.haml b/app/views/static/terms.haml index 6650c250..b72c1002 100644 --- a/app/views/static/terms.haml +++ b/app/views/static/terms.haml @@ -1,5 +1,5 @@ - provide(:title, generate_title('Terms of Service')) -.container.container--main +.container-lg.container--main .card .card-body = raw_markdown_io 'service-docs/en/policy/terms.md' diff --git a/spec/factories/theme.rb b/spec/factories/theme.rb new file mode 100644 index 00000000..4339377a --- /dev/null +++ b/spec/factories/theme.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :theme do + primary_color { 9_342_168 } + primary_text { 16_777_215 } + danger_color { 14_257_035 } + danger_text { 16_777_215 } + success_color { 12_573_067 } + success_text { 16_777_215 } + warning_color { 14_261_899 } + warning_text { 16_777_215 } + info_color { 9_165_273 } + info_text { 16_777_215 } + dark_color { 6_710_886 } + dark_text { 15_658_734 } + raised_background { 16_777_215 } + background_color { 13_026_795 } + body_text { 3_355_443 } + muted_text { 3_355_443 } + input_color { 15_789_556 } + input_text { 6_710_886 } + raised_accent { 16_250_871 } + light_color { 16_316_922 } + light_text { 0 } + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 6bdc4702..fa6f8b4e 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -3,6 +3,40 @@ require "rails_helper" describe ApplicationHelper, :type => :helper do + describe '#nav_entry' do + it 'should return a HTML navigation item which links to a given address' do + allow(self).to receive(:current_page?).and_return(false) + expect(nav_entry('Example', '/example')).to( + eq('') + ) + end + + it 'should return with an active attribute if the link matches the current URL' do + allow(self).to receive(:current_page?).and_return(true) + expect(nav_entry('Example', '/example')).to( + eq('') + ) + end + + it 'should include an icon if given' do + allow(self).to receive(:current_page?).and_return(false) + expect(nav_entry('Example', '/example', icon: 'beaker')).to( + eq('') + ) + end + + it 'should include a badge if given' do + allow(self).to receive(:current_page?).and_return(false) + expect(nav_entry('Example', '/example', badge: 3)).to( + eq('') + ) + + expect(nav_entry('Example', '/example', badge: 3, badge_color: 'primary', badge_pill: true)).to( + eq('') + ) + end + end + describe "#bootstrap_color" do it 'should map error and alert to danger' do expect(bootstrap_color("error")).to eq("danger") diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 3776192d..663094d9 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -141,4 +141,54 @@ describe ThemeHelper, :type => :helper do end end end + + describe '#theme_color' do + subject { helper.theme_color } + + context 'when user is signed in' do + let(:user) { FactoryBot.create(:user) } + let(:theme) { FactoryBot.create(:theme, user: user) } + + before(:each) do + user.theme = theme + user.save! + sign_in(user) + end + + it 'should return the user theme\'s primary color' do + expect(subject).to eq('#8e8cd8') + end + end + + context 'user is not signed in' do + it 'should return the default primary color' do + expect(subject).to eq('#5e35b1') + end + end + end + + describe '#mobile_theme_color' do + subject { helper.mobile_theme_color } + + context 'when user is signed in' do + let(:user) { FactoryBot.create(:user) } + let(:theme) { FactoryBot.create(:theme, user: user) } + + before(:each) do + user.theme = theme + user.save! + sign_in(user) + end + + it 'should return the user theme\'s background color' do + expect(subject).to eq('#c6c5eb') + end + end + + context 'user is not signed in' do + it 'should return the default background color' do + expect(subject).to eq('#f0edf4') + end + end + end end diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb new file mode 100644 index 00000000..a170c1d9 --- /dev/null +++ b/spec/models/theme_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(Theme, type: :model) do + context 'user-defined theme' do + let(:user) { FactoryBot.create(:user) } + let(:theme) { FactoryBot.create(:theme, user: user) } + + describe '#theme_color' do + subject { theme.theme_color } + + it 'should return the theme\'s primary colour as a hex triplet' do + expect(subject).to eq('#8e8cd8') + end + end + + describe '#mobile_theme_color' do + subject { theme.mobile_theme_color } + + it 'should return the theme\'s background colour as a hex triplet' do + expect(subject).to eq('#c6c5eb') + end + end + end +end \ No newline at end of file