Merge pull request #184 from Retrospring/mobile-layout

Adjust site layout to be nicer to use on smaller screens
This commit is contained in:
Karina Kwiatek 2021-08-13 12:11:38 +02:00 committed by GitHub
commit 89ce3e6e53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 311 additions and 47 deletions

View File

@ -39,6 +39,7 @@
@import @import
"overrides/alerts", "overrides/alerts",
"overrides/badges",
"overrides/bootstrap-datetimepicker", "overrides/bootstrap-datetimepicker",
"overrides/buttons", "overrides/buttons",
"overrides/colors", "overrides/colors",
@ -81,6 +82,7 @@
"components/inbox-entry", "components/inbox-entry",
"components/jumbotron", "components/jumbotron",
"components/locales", "components/locales",
"components/mobile-nav",
"components/notifications", "components/notifications",
"components/profile", "components/profile",
"components/question", "components/question",

View File

@ -11,7 +11,7 @@
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
position: fixed; position: fixed;
bottom: 0px; bottom: unquote('calc(#{$navbar-height} + env(safe-area-inset-bottom))');
right: 0px; right: 0px;
margin-right: 7px; margin-right: 7px;
margin-bottom: 7px; margin-bottom: 7px;

View File

@ -1,4 +1,10 @@
.container--main { .container--main {
padding-top: map-get($spacers, 3); padding-top: map-get($spacers, 3);
padding-bottom: 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))');
} }

View File

@ -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;
}

View File

@ -3,5 +3,15 @@ body {
word-wrap: break-word; word-wrap: break-word;
color: RGB(var(--body-text)); color: RGB(var(--body-text));
background-color: var(--background); background-color: var(--background);
@include media-breakpoint-up('lg') {
padding-top: $navbar-height; padding-top: $navbar-height;
}
@include media-breakpoint-down('md') {
padding-bottom: $navbar-height;
}
&.not-logged-in {
padding-top: $navbar-height;
padding-bottom: 0;
}
} }

View File

@ -0,0 +1,6 @@
@each $color in $color-names {
.badge-#{$color} {
color: var(--#{$color});
background-color: RGB(var(--#{$color}-text));
}
}

View File

@ -16,14 +16,17 @@ module ApplicationHelper
].compact.join(" ") ].compact.join(" ")
unless options[:icon].nil? 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 end
unless options[:badge].nil? unless options[:badge].nil?
# TODO: make this prettier? badge_class = "badge"
body << " #{ badge_class << " badge-#{options[:badge_color]}" unless options[:badge_color].nil?
content_tag(:span, options[:badge], class: ("badge#{ badge_class << " badge-pill" if options[:badge_pill]
" badge-#{options[:badge_color]}" unless options[:badge_color].nil? body += " #{content_tag(:span, options[:badge], class: badge_class)}"
}"))}"
end end
content_tag(:li, link_to(body.html_safe, path, class: "nav-link"), class: classes) content_tag(:li, link_to(body.html_safe, path, class: "nav-link"), class: classes)

View File

@ -50,12 +50,21 @@ module ThemeHelper
def theme_color def theme_color
theme = get_active_theme theme = get_active_theme
if theme if theme
"##{get_hex_color_from_theme_value(theme.primary_color)}" theme.theme_color
else else
'#5e35b1' '#5e35b1'
end end
end end
def mobile_theme_color
theme = get_active_theme
if theme
theme.mobile_theme_color
else
'#f0edf4'
end
end
def get_active_theme def get_active_theme
if @user&.theme if @user&.theme
if user_signed_in? if user_signed_in?

View File

@ -20,4 +20,8 @@ class Theme < ApplicationRecord
def theme_color def theme_color
('#' + ('0000000' + primary_color.to_s(16))[-6, 6]) ('#' + ('0000000' + primary_color.to_s(16))[-6, 6])
end end
def mobile_theme_color
('#' + ('0000000' + background_color.to_s(16))[-6, 6])
end
end end

View File

@ -1,5 +1,5 @@
- provide(:title, generate_title('Edit announcement')) - provide(:title, generate_title('Edit announcement'))
.container.container--main .container-lg.container--main
.card .card
.card-body .card-body
= bootstrap_form_for(@announcement, url: { action: 'update' }, method: 'PATCH') do |f| = bootstrap_form_for(@announcement, url: { action: 'update' }, method: 'PATCH') do |f|

View File

@ -1,5 +1,5 @@
- provide(:title, generate_title('Announcements')) - provide(:title, generate_title('Announcements'))
.container.container--main .container-lg.container--main
- @announcements.each do |announcement| - @announcements.each do |announcement|
.card .card
.card-body .card-body

View File

@ -1,5 +1,5 @@
- provide(:title, generate_title('Add new announcement')) - provide(:title, generate_title('Add new announcement'))
.container.container--main .container-lg.container--main
.card .card
.card-body .card-body
= bootstrap_form_for(@announcement, url: { action: 'create' }) do |f| = bootstrap_form_for(@announcement, url: { action: 'create' }) do |f|

View File

@ -1,4 +1,4 @@
- provide(:title, answer_title(@answer)) - provide(:title, answer_title(@answer))
- provide(:og, answer_opengraph(@answer)) - provide(:og, answer_opengraph(@answer))
.container.container--main .container-lg.container--main
= render 'answerbox', a: @answer, display_all: @display_all = render 'answerbox', a: @answer, display_all: @display_all

View File

@ -3,7 +3,11 @@
%head %head
%meta{ charset: 'utf-8' } %meta{ charset: 'utf-8' }
%meta{ 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' } %meta{ 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no' } %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 } %meta{ name: 'theme-color', content: theme_color }
%link{ rel: 'apple-touch-icon', href: '/apple-touch-icon-precomposed.png' } %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: '/images/favicon/favicon-16.png', sizes: '16x16' }
@ -19,7 +23,7 @@
= csrf_meta_tags = csrf_meta_tags
= yield(:og) = yield(:og)
= yield(:meta) = yield(:meta)
%body %body{ class: user_signed_in? ? '' : 'not-logged-in' }
- if user_signed_in? - if user_signed_in?
= render 'navigation/main' = render 'navigation/main'
- else - else

View File

@ -1,4 +1,4 @@
.container.container--main .container-lg.container--main
.row .row
.col-md-3.col-sm-4.d-none.d-sm-block .col-md-3.col-sm-4.d-none.d-sm-block
= render 'shared/sidebar' = render 'shared/sidebar'

View File

@ -1,4 +1,4 @@
.container.container--main .container-lg.container--main
.row .row
.col-md-3.col-xs-12.col-sm-4.order-2.order-sm-1 .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 = render 'inbox/sidebar', delete_id: @delete_id, disabled: @disabled, inbox_count: @inbox_count

View File

@ -1,5 +1,5 @@
= render 'navigation/moderation' = render 'navigation/moderation'
.container.container--main .container-lg.container--main
.row .row
.col-md-3.col-sm-4.col-xs-12 .col-md-3.col-sm-4.col-xs-12
= render 'tabs/moderation' = render 'tabs/moderation'

View File

@ -1,5 +1,5 @@
= render 'navigation/notification' = render 'navigation/notification'
.container.container--main .container-lg.container--main
.row .row
.col-md-3.col-xs-12.col-sm-4 .col-md-3.col-xs-12.col-sm-4
= render 'tabs/notifications' = render 'tabs/notifications'

View File

@ -1,4 +1,4 @@
.container.container--main .container-lg.container--main
.row .row
.col-md-3.col-xs-12.col-sm-4 .col-md-3.col-xs-12.col-sm-4
= render 'tabs/settings' = render 'tabs/settings'

View File

@ -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'

View File

@ -1,26 +1,5 @@
%nav.navbar.navbar-themed.navbar-expand-lg.bg-primary.fixed-top{ role: :navigation } = render 'navigation/desktop'
.container{ class: ios_web_app? ? 'ios-web-app' : '' } = render 'navigation/mobile'
%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 'modal/ask' = render 'modal/ask'
%button.btn.btn-primary.btn-fab.d-block.d-sm-none{ data: { target: '#modal-ask-followers', toggle: :modal }, type: 'button' } %button.btn.btn-primary.btn-fab.d-block.d-sm-none{ data: { target: '#modal-ask-followers', toggle: :modal }, type: 'button' }

View File

@ -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) }

View File

@ -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'

View File

@ -1,4 +1,4 @@
.container.container--main .container-lg.container--main
.row.justify-content-center .row.justify-content-center
.col-md-5.totp-setup__recovery-container .col-md-5.totp-setup__recovery-container
.card .card

View File

@ -1,5 +1,5 @@
- provide(:title, generate_title('Privacy Policy')) - provide(:title, generate_title('Privacy Policy'))
.container.container--main .container-lg.container--main
.card .card
.card-body .card-body
= raw_markdown_io 'service-docs/en/policy/privacy.md' = raw_markdown_io 'service-docs/en/policy/privacy.md'

View File

@ -1,5 +1,5 @@
- provide(:title, generate_title('Terms of Service')) - provide(:title, generate_title('Terms of Service'))
.container.container--main .container-lg.container--main
.card .card
.card-body .card-body
= raw_markdown_io 'service-docs/en/policy/terms.md' = raw_markdown_io 'service-docs/en/policy/terms.md'

27
spec/factories/theme.rb Normal file
View File

@ -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

View File

@ -3,6 +3,40 @@
require "rails_helper" require "rails_helper"
describe ApplicationHelper, :type => :helper do 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('<li class="nav-item "><a class="nav-link" href="/example">Example</a></li>')
)
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('<li class="nav-item active "><a class="nav-link" href="/example">Example</a></li>')
)
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('<li class="nav-item "><a class="nav-link" href="/example"><i class="fa fa-beaker"></i> Example</a></li>')
)
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('<li class="nav-item "><a class="nav-link" href="/example">Example <span class="badge">3</span></a></li>')
)
expect(nav_entry('Example', '/example', badge: 3, badge_color: 'primary', badge_pill: true)).to(
eq('<li class="nav-item "><a class="nav-link" href="/example">Example <span class="badge badge-primary badge-pill">3</span></a></li>')
)
end
end
describe "#bootstrap_color" do describe "#bootstrap_color" do
it 'should map error and alert to danger' do it 'should map error and alert to danger' do
expect(bootstrap_color("error")).to eq("danger") expect(bootstrap_color("error")).to eq("danger")

View File

@ -141,4 +141,54 @@ describe ThemeHelper, :type => :helper do
end end
end 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 end

26
spec/models/theme_spec.rb Normal file
View File

@ -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